The core and the headless tools no longer use mutexes, condition variables,
spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through
libretro-common's retro_atomic and rthreads plus a few small pieces on top of
them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and
ReaderGate. Every wait blocks until it's woken; nothing polls on a timer.
State that only one thread touches just loses its lock. The PSP filesystem,
sceIo and the savedata dialog now belong to the emulation thread: async reads
and writes run when they're queued and complete later in emulated time, and
the IO and savedata threads are gone. The friend finder only talks to the
adhoc server and hands what it gets to the emulation thread. GPU init always
runs on the thread that starts the game.
The Vulkan render and compile queues, pipeline and render pass caches, delete
list and SPIR-V cache, the GL render queue, the software renderer's jit caches,
the file loader caches, the web debugger, UPnP, input, savestates, logging and
the GE debugger all hand off through queues and atomics. The IR jit's spinning
profiler thread goes.
A CPU thread with nothing to run now sleeps until work is queued instead of
spinning. Headless with --debugger waits for a client while stopped, and ends
the run once every client that connected has left. Web server shutdown keeps
servicing the CPU queue until the server has finished.
The core no longer sleeps to pace itself: the frontend runs retro_run at
its own rate, so the frame throttle, the idle-vblank delay and the real
clock sync event (and its core option) are gone, and a stepping CPU hands
straight back instead of waiting 2 ms. Waits that polled now park on a
lock-free eventcount through Common/Thread/ParkingLot and are woken by
whoever changes the state: a paused GL emu thread, which also queues a
marker behind its last frame so pausing runs the render queue up to it
instead of polling; async I/O results; the software renderer when its
queue is full; Vulkan pipeline compiles; and debugger requests once
stepping ends. Boot joins the loader thread, cache shutdown joins its
read-ahead thread, and the slow-I/O simulation is removed.
The path browser, the HTTP and WebSocket servers, the Windows console and
the game info cache are left out of the core build. libretro/test gains
lrgltest, which drives the GL backend on a surfaceless EGL context through
every path that pauses or stops the emu thread; run.sh and the image
checks CI job run it on llvmpipe.
System_AudioPushSamples converts straight into the output buffer: the
block loop never advanced the source pointer and copied numSamples*2
int16s out of a 2048-sample stack buffer, a stack OOB read for any push
over 1024 frames. Realloc failure drops samples instead of crashing and
log_cb is NULL-checked. retro_serialize refuses to write a state larger
than the frontend's buffer, retro_get_memory_data/size return nothing
until PSP memory is mapped, deferred unserialize no longer leaks the
previous buffer, and the cheat entry points stop leaking a CWCheatEngine
per call.
The emu thread acknowledges a pause with a CAS so it cannot clobber a
concurrent quit request. EmuThreadStop stops and joins from any live
state, so unloading while paused no longer leaves a thread running over
freed state. EmuThreadPause pumps queued frames until the thread parks,
via a new non-blocking GLRenderManager::HasQueuedThreadWork() check,
instead of draining one frame, which deadlocked when the thread was
blocked on a readback sync in the next frame. retro_run resumes a paused
thread and pauses it before check_variables() rewrites g_Config,
retro_reset stops it before PSP_Shutdown(), and the sleep_ms(4) after
resume is gone.
These own GPU objects, memory or refcounts in their destructors (or assert
there that they were torn down), so a copy would double-free. Nothing copies
them today; this keeps it that way. The manager base classes cover every
backend's subclass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DrawEngineGLES already knows that every index it generates is below the
decoded/transformed vertex count, but only passed the count to
glDrawElements. Drivers that need the vertex range before vertex
shading, like Mesa's Panfrost, then scan the index data on the CPU for
every draw, and their min/max caches can't help since the index push
buffer is rewritten every frame.
Only used for single-instance draws on desktop GL or GLES3, and only
when the caller provides the range, so other DrawIndexed callers are
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
imgui 1.92 hands the backend a list of dirty rectangles when its font atlas
grows, but thin3d could only replace a whole mip level, so every new glyph
re-uploaded the entire atlas.
Adds DrawContext::UpdateTextureRegions, taking a batch of regions so each
backend can submit them together:
- Vulkan: packs the regions into the push pool and issues a single
vkCmdCopyBufferToImage from the init command buffer, transitioning only
the level being written.
- OpenGL: new TEXTURE_SUBIMAGE init step. The existing sub-image path is a
render command needing an active render pass and a texture slot, which
doesn't fit here. Rows are packed caller-side since GLES2 lacks
GL_UNPACK_ROW_LENGTH.
- D3D11: UpdateSubresource with a box, no staging texture needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GLRenderManager is documented as "emu thread records, render thread executes",
but GL has to record device object creation as init steps rather than just doing
it, and InitGPU() runs on the ExecLoader thread - GPU_GLES's constructor builds
DrawEngineGLES, whose InitDeviceObjects() reaches initSteps_ through
CreatePushBuffer and CreateInputLayout. The emu thread is still drawing the
loading screen into the same FastVec until the loader thread is joined, so two
concurrent push_uninitialized() can both reallocate, and one writes its step into
a freed buffer - losing a shader or buffer creation, or scribbling an owned
pointer into freed memory.
frameData_[].activePushBuffers is genuinely three-threaded too: inserted into by
whoever creates a push buffer, erased on the render thread via GLDeleter, and
walked on the render thread each frame.
A mutex each, uncontended in practice. Note this makes the existing access safe
rather than fixing the layering - Vulkan avoids the problem by creating objects
directly and deferring the rest to FinishInitOnMainThread, which GPU_GLES has
never had. Moving GL's device object creation there would be the better fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
Reported by Joseph on Discord.
Sometimes, things could align perfectly so allocations happend exactly
at the end of a pushbuffer. At the same time, we allow our vertex decoder to write an extra few
bytes if it needs to for speed. Unfortunately I missed this interaction,
resulting in some uncommon crashes that were especially common with
heavy-geometry things like modified GTA LCS with PS2 assets, for
example.
The problem was reported with Vulkan, but our OpenGL backend had the
same issue too.
It doesn't try to insert or fetch missing things itself, re-fetching is up to the caller.
This will be required for handling the many achievement icons.
Saving/loading the cache to a single file on disk is implemented but not
hooked up yet. It works without it, though of course will have to
re-fetch things on the next startup.
Accessed from the in-game dev menu just like the Vulkan frame profiler.
With this we can easily see that actually submitting the GL commands is often the bottleneck on old
devices like a Galaxy S3.