call_gate_t is the same counters as before, zero-filled when open.
The gates a thread is inside are now a list of call_gate_frame_t on
the callers' own stacks, linked through an rthreads TLS key, instead of
a thread_local array: no nesting limit and nothing to free per thread.
The TLS key and the shared eventcount are made on first use and
published with a CAS, so racing threads never wait on each other.
Builds rthreads with HAVE_THREAD_STORAGE for the TLS key.
Waits that polled now block on an eventcount that the waited-on thread
notifies:
- Booting in dual core waits for the CPU thread to reach its run loop
instead of polling every millisecond.
- AsyncShaderCompiler::WaitUntilCompletion wakes as each worker
finishes an item instead of every 33 ms.
Timed sleeps go through Common::SleepUntil and SleepCurrentThreadUs,
built on retro_sleep_us: the CoreTiming throttle, PrecisionTimer, real
Wiimote report pacing, Bluetooth timed transfers, the Linux L2CAP
connect retry and the BBA read thread's idle wait.
FrameTiming::ThrottleFrame had no callers and is deleted.
Every std::mutex, lock_guard, unique_lock, scoped_lock,
condition_variable, call_once and semaphore left in Source/Core is
replaced by a lock-free design:
- Input: the device list, control bindings, stick calibration and
default device are published snapshots read under a quiesce; a
rebind requested while another thread is rebinding is handed to
that thread instead of waited for.
- WiimoteReal: the pool thread owns the slots and runs commands from
an MPSC queue; the active Wiimotes are atomic slots.
- EXI/SI/IOS devices: socket and packet traffic goes through SPSC
rings or MPSC handoffs owned by one thread; USB scanning publishes
its device list, and libusb transfers drain through a CallGate.
- Memory cards: the flush thread applies change records queued by
the CPU thread.
- Custom assets: loads are scheduled as a published batch with an
atomic cursor, results come back over an MPSC stack, and busy
assets are skipped and retried.
- D3D state and GL program caches use insert-only atomic maps
(Common/AtomicHashMap.h).
- Logging appends with O_APPEND / FILE_APPEND_DATA; FatFs reports
busy instead of waiting; per-socket WSAStartup/WSACleanup.
Deleted as unused by the libretro core: the profiler, the FIFO
recorder, the volume verifier, the touch InputOverrider and the
GameCube adapter's libusb path.
CPU: the state machine drops its two mutexes and two condition
variables. The state is one atomic word, which the JIT still reads in
place; PowerDown sticks through a CAS. The CPU thread marks itself
active and then rechecks for a pause or a state change, and a pausing
thread stores its request and then waits for the flag to drop, a
Dekker pair, so neither misses the other. Waits park on eventcounts.
A step request and its Event travel as one exchanged pointer, pending
jobs are an MPSC stack run in order, and SetStepping and PauseAndLock
still run one at a time, through a claimed flag.
Core: the recursive core mutex goes. Stop moves the state with a CAS,
and PauseAndLock pairs are already serialized by the CPU's claim. Host
jobs are an MPSC stack the host thread moves, in order, into its own
queue, which keeps HostDispatchJobs re-entrant. RunOnCPUThread waits
on a Common::Event instead of a binary semaphore, and OneShotEvent is
removed.
State: the transferable shared mutex that counted save tasks is a
counter and an eventcount; the slot readers and the flush hook wait for
it to reach zero. TransferableSharedMutex is removed. WaitableFlag
parks on an eventcount.
PixelEngine: the token, its interrupt flags and the raised-event flag
are one atomic word; the video thread merges into it and schedules the
event only on the edge, the CPU thread takes the flags with a CAS.
Savestates keep the old fields.
VertexLoaderManager: the loader map is a fixed set of buckets, each a
list pushed with a CAS and read without a lock by the main and the
preprocessing thread; two threads making the same loader keep one.
AsyncShaderCompiler: the pending queue is the video thread's own. It
hands the most urgent items to two slots per worker, and finished items
come back on an MPSC stack, so neither side takes a lock.
OSD messages are posted to an MPSC stack and taken in by the video
thread when it draws; a screenshot request is one exchanged string.
Also removed, being unreachable: the surface change and resize requests
nothing sends (and the swap mutex guarding them), the ImGui keyboard
and mouse input nothing feeds (and the ImGui mutex, as only the video
thread touches the context now), ClearMessages, and the libretro Vulkan
WaitForPresentation with its mutex and condition variable.
Two small primitives carry it:
Quiesce (C89) frees read-mostly shared data without locks. Readers
bracket an access with quiesce_enter/exit; a writer swaps in a new
pointer and retires the old one, and whichever thread sees the reader
count reach zero frees it. Nobody waits, and a reader that retires
something from inside, such as a callback removing itself, only defers
the free.
CallGate lets a callback be unregistered while other threads may be
calling it. Calls enter and leave the gate with one seq_cst add each;
closing it shuts out new calls and returns once the ones on other
threads have left, parking on an eventcount meanwhile, so the owner can
free what the callback captured. A callback closing its own gate does
not wait on itself.
Config: the layers are a fixed array of published pointers indexed by
LayerType instead of a map under a shared_mutex, and the change
callbacks a fixed array of slots claimed by CAS instead of a vector
under a recursive_mutex. Readers, OnConfigChanged among them, walk
them under the quiesce; RemoveConfigChangedCallback closes the
callback's gate, so as before nothing it captured is used once it
returns.
Info's cached value drops its spin mutex. A value of four bytes or
less shares one 64-bit word with its config version, so Config::Get
reads it with a single load and updates it with a CAS; a wider one is a
published copy freed through the quiesce. Common/Mutex.h had no other
user and is removed.
HookableEvent publishes its listeners as an immutable array, replaced
whole on register and unregister, so Trigger takes no lock. A hook
registered from inside a callback is first called by the next Trigger;
destroying a hook while another thread runs its callback waits for that
call, as before.
Checked with a stress harness under TSan and ASan/UBSan: three readers
against two writers retiring 400000 objects, and hooks registered and
destroyed 20000 times against two threads triggering.
None of it is built. USE_RETRO_ACHIEVEMENTS is never defined, since a
libretro frontend runs achievements itself; what stays is the stub
AchievementManager the rest of the core already compiled against.
Source/UnitTests has had no build since the core moved to
Makefile.common.
They also held a good share of the remaining std::mutex users, which
the following commits take out of the code that is built.
Both fed user-written GLSL to the backends at runtime, which the move to
bytecode emitted straight from the shader generators leaves no compiler
for. The frontend's own shader pipeline covers post-processing.
The presentation pass keeps a plain copy and the built-in correction
shader for output resampling, colour space and gamma correction and HDR
scaling, now compiled in rather than loaded from Sys/Shaders. The
graphics-mod system keeps its draw, EFB, projection and texture actions;
custom pipelines, materials, shader, mesh and sampler assets, the custom
constant buffers in every backend and the two mods built on them are
gone, along with the shader generators' custom-shader hooks.
No std::thread, std::async, std::future or std::promise is left. Each
thread is an sthread with a plain entry point: Common::ThreadEntry<&C::F>
turns a member function into one, Common::JoinThread joins and clears a
handle, and Common::StartDetachedThread covers the fire-and-forget users.
Common/ParallelFor.{h,c} (C89) runs fn(ctx, i) for i in [0, count) on a
persistent pool of one worker per extra core plus the caller. Jobs are
claimed with an atomic counter, workers park on an eventcount between
batches, and the batch hand-off is a seq_cst users/active pair, so there
is no lock anywhere. A caller that finds the pool busy runs its batch
serially instead of waiting. retro_deinit joins the pool.
- VolumeWii::HashGroup spawned 64 threads per 2 MiB group and
EncryptGroup another one per core; both are now parallel_for batches
on the pool. The H1/H2 tail is a few small hashes and runs inline.
- VolumeVerifier spawned up to five std::async threads per chunk; the
chunk's tasks now go to one persistent worker that fans them out with
parallel_for, still overlapping the next read. The Redump lookup is an
sthread joined in Finish or the destructor.
- The dual-core "GPU thread" in Core only set a thread_local on itself
and exited, since the frontend runs the GPU loop. It is gone, along
with the non-libretro branches of Core.cpp.
- The emu thread is started through Core::StartEmuThread.
- CPU core counts come from cpu_features_get_core_amount().
None of these run in the core: UPnP served NetPlay; the DInput, XInput,
WGInput, Win32, Quartz, Xlib, evdev and Pipes input backends are never
enabled, since input comes from libretro; and disc conversion, with its
multithreaded compressor, was only reached from DolphinTool. The WIA,
RVZ and GCZ readers stay.
Common::Event is an atomic flag with a retro_eventcount to park on, so
Set takes no lock and makes no syscall while nobody waits. BlockingLoop
loses both of its mutexes: waiters park on an eventcount and re-check
IsDone(), so any number may wait at once, and Prepare hands setup to
the one thread that moves the loop out of the stopped state.
The thread helpers name, yield and sleep through rthreads and
retro_timers; the unused affinity and SwitchCurrentThread helpers go.
Every Common::SPSCQueue goes, and with it the mutexes in front of the
multi-producer ones:
- WorkQueueThread runs on an sthread fed by an mpsc_stack, so any
thread pushes without a lock, and parks on retro_eventcount.
- AsyncRequests hands requests to the video thread through a
retro_waitable_spsc of pointers; a blocking request lives on the
caller's stack.
- CoreTiming's off-thread events and the DVD thread's results move to
mpsc_stack, dropping the CoreTiming write lock.
- Real Wiimote reports cross threads as fixed 32-byte records in
retro_spsc rings; the read thread receives straight into the ring.
- Bluetooth passthrough frees transfers through an mpsc_stack whose
nodes live in the transfers' own buffers, queues incoming packets in
mirrored retro_spsc rings and hands ACL data to IOS in place.
- PerformanceTracker's frame times move to a retro_spsc ring.
The core never creates a NetPlay client or server; only the standalone
frontends did. Drop NetPlay with what only it used: the traversal
client, ENet, the QoS session, the NetPlay config layer and settings,
the chat and golf overlays, the netplay index, the save sync plumbing
in BootSessionData and WiiRoot, the disc sync hashes, and the Android
app's netplay feature and JNI bindings.
From RetroArch's libretro-common: rthreads, retro_eventcount,
retro_spsc, retro_waitable_spsc, mpsc_stack and retro_atomic, with the
units they need (features_cpu, rtime, compat_strl), built as their own
library group.
The core only ever outputs through libretro, so drop the ALSA, cubeb,
null, OpenAL, OpenSL ES, PulseAudio and WASAPI streams, the backend
selection, and the vendored cubeb and OpenAL. SoundStream is now one
non-virtual class, implemented by the libretro output in
DolphinLibretro/Audio.cpp.
Also drop the settings only the backends read (backend, volume, mute,
mute on disabled speed limit, latency, WASAPI device) and the volume
and mute helpers. The mute-on-fast-forward toggle never had an effect,
as the libretro stream ignored volume changes.
The GameCube microphone, Wii Speak and Logitech mic keep their libretro
microphone path; the unbuilt cubeb capture path and its Windows COM
worker thread go.
Dolby Pro Logic II decoding (SurroundDecoder, FreeSurround) stays.
Drop DolphinAnalytics and Common/Analytics along with every game-quirk,
game-start, performance-sample and GPU-bug-override report, the
Main.Analytics settings, and the Android opt-in dialog, settings
entries, JNI hooks and session-timestamp tracking that only fed it.
DriverDetails loses its to_string helpers and stored driver name,
which only built the bug-override report.
Discord presence was already disabled in the core (Host callbacks were
stubs and Boot turned it off); drop UICommon's DiscordPresence, the
vendored discord-rpc, its settings and the Host_UpdateDiscord* hooks.
The DolphinDevice Discord IOCTLs now answer IPC_EACCES, as Dolphin does
with presence disabled.
X11Utils was only used by DolphinNoGUI, a standalone frontend, so both
go.
UpdaterCommon, WinUpdater, MacUpdater and UICommon's AutoUpdate only
served the standalone Dolphin frontends; RetroArch updates the core.
Also drop the AutoUpdate settings, the update-track build string and
the u32 WindowsRegistry::ReadValue instantiation only WinUpdater used.
The core runs only inside a libretro frontend. The Qt frontend and its
submodule go, together with what only it used: the Qt DBus screensaver
inhibit, the JIT's single-block erase, the string translator hook, the
Qt translation catalogs and Transifex config, the desktop packaging
(NSIS, RPM, Flatpak) and the autoupdate overview.
The core takes its input from the libretro frontend and never builds the
SDL controller backend. The SDL submodule, the backend, its config entries
and the SDL hints and version bits of the Qt frontend go.
The static Makefile is the only build. GitLab CI and the GitHub workflows
build with it, with the same lanes and toolchains; the MSVC workflow and
the CMake-only macOS universal-binary script go, as nothing builds them.
The Readme describes the Makefile build.
A bare make picks the Windows build from OS=Windows_NT, which every MSYS2
environment sets whatever uname -s reports, or from a MinGW-w64 CC. The
Windows build defaults to gcc/g++ instead of make's built-in cc.
Builds parallel_gamecubewii_libretro for every lane .gitlab-ci.yml covers:
unix (Linux x86_64/aarch64), webos, win64 (MinGW-w64), osx (x86_64/arm64,
CROSS_COMPILE), ios-arm64, tvos-arm64, android-arm64 and android-x86_64.
The source lists and per-library flags follow what the CMake LIBRETRO=ON
build links into the core, without the units only the standalone frontends
use. The per-platform configure results for curl, libusb, libiconv and mGBA
are checked in under Externals/<library>/makefile; the remaining generated
files are produced by the Makefile under obj/gen.
The Android input backend's JNI entry points belong to the Android app and
are left out of the core, which links with --no-undefined everywhere.
The Windows code builds against MinGW-w64 headers older than 12.0 as well:
D3D12 includes d3d12sdklayers.h explicitly, and the constants, declarations
and const-correct pointer types those headers lack for D3D12, device
notifications, the Wii Remote Bluetooth path, the IOS IP stack and thread
naming are provided locally.
A region-free title runs in the fallback region, which Dolphin takes
from the system locale. Kodi on LibreELEC runs with no locale, so that
is PAL, and Japanese WiiWare marked region-free hangs on a black screen.
Run such a title in the region its game ID names, for that run only.
Titles with a region of their own, and IDs that name none, are
unchanged.
Dolphin fitted its picture into the frame by the Aspect Ratio mode, bars
and all, and the frontend scaled the frame to 4:3 or 16:9 once more, so
every mode but Stretch came out squeezed or stretched (Custom, Custom
Stretch and Raw the most). Dolphin now always stretches over the frame,
and the core gives the frontend the aspect ratio Dolphin works out for the
chosen mode (Presenter::CalculateDrawAspectRatio for a given mode), again
whenever it changes. Stretch and VR are as before.
Vulkan hands the frontend the whole swapchain image Dolphin presented
into, as D3D11 and OpenGL do with their backbuffer, instead of the EFB's
size (640x528 against a 640x480 image), which squeezed the picture.
StateManager::Restore() bound back only slot 0 of the vertex shader's
constant buffers and neither the custom constants at slot 2 nor the
framebuffer, while it told Apply() they were all the core's. A shader
preset binds its uniform and push constant buffers at slots 0 and 1, so
the core went on drawing with the preset's buffer at slot 1: a black
picture with sound, which stayed black after the preset was turned off
(#378, #452). The frontend's passes also found the core's 64-byte
buffer at the vertex shader's slot 1.