mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-16 16:00:30 +02:00
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.
364 lines
11 KiB
C++
364 lines
11 KiB
C++
// Copyright (c) 2013- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include <retro_atomic.h>
|
|
|
|
#include "Common/Log.h"
|
|
#include "Common/Thread/MpscQueue.h"
|
|
#include "Common/Thread/ParkingLot.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/HW/Display.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "GPU/Debugger/Stepping.h"
|
|
#include "GPU/GPUState.h"
|
|
|
|
namespace GPUStepping {
|
|
|
|
enum PauseAction {
|
|
PAUSE_CONTINUE,
|
|
PAUSE_BREAK,
|
|
PAUSE_GETOUTPUTBUF,
|
|
PAUSE_GETFRAMEBUF,
|
|
PAUSE_GETDEPTHBUF,
|
|
PAUSE_GETSTENCILBUF,
|
|
PAUSE_GETTEX,
|
|
PAUSE_GETCLUT,
|
|
PAUSE_SETCMDVALUE,
|
|
PAUSE_FLUSHDRAW,
|
|
};
|
|
|
|
enum RequestState {
|
|
REQUEST_PENDING,
|
|
REQUEST_RUNNING,
|
|
REQUEST_DONE,
|
|
REQUEST_WITHDRAWN,
|
|
};
|
|
|
|
// One debugger request for the emu thread. Shared by the requester and the queue, freed by the last.
|
|
struct PauseRequest {
|
|
PauseAction action;
|
|
GPUDebugFramebufferType bufferType = GPU_DBG_FRAMEBUF_RENDER;
|
|
int bufferLevel = 0;
|
|
u32 setCmdValue = 0;
|
|
int epoch = 0;
|
|
// Written by the emu thread before REQUEST_DONE.
|
|
bool result = false;
|
|
bool wasFramebuffer = false;
|
|
retro_atomic_int_t state{ REQUEST_PENDING };
|
|
retro_atomic_int_t refs{ 1 };
|
|
};
|
|
|
|
static retro_atomic_int_t isStepping{ 0 };
|
|
// Number of times we've entered stepping, to detect a resume asynchronously.
|
|
static retro_atomic_int_t stepCounter{ 0 };
|
|
// Set to resume running the GE (PAUSE_CONTINUE), cleared on entering stepping (PAUSE_BREAK).
|
|
static retro_atomic_int_t continueRequested{ 1 };
|
|
// Bumped on resume and reset: requests from before are withdrawn rather than run at some later break.
|
|
static retro_atomic_int_t requestEpoch{ 0 };
|
|
// Requests posted by debugger threads, drained on the emu thread.
|
|
static MpscQueue<PauseRequest *> requests;
|
|
// Threads waiting in RequestPauseAction(); also the address they park on.
|
|
static retro_atomic_int_t requestsWaiting{ 0 };
|
|
|
|
// Many things need to run on the GPU thread. For example, reading the framebuffer.
|
|
// A message system is used to achieve this (temporarily "unpausing" the thread.)
|
|
// Below are the results of actions, written on the emu thread.
|
|
|
|
static GPUDebugBuffer bufferFrame;
|
|
static GPUDebugBuffer bufferDepth;
|
|
static GPUDebugBuffer bufferStencil;
|
|
static GPUDebugBuffer bufferTex;
|
|
static GPUDebugBuffer bufferClut;
|
|
|
|
// This is used only to highlight differences. Should really be owned by the debugger.
|
|
static GEState lastGState;
|
|
|
|
const char *PauseActionToString(PauseAction action) {
|
|
switch (action) {
|
|
case PAUSE_CONTINUE: return "CONTINUE";
|
|
case PAUSE_BREAK: return "BREAK";
|
|
case PAUSE_GETOUTPUTBUF: return "GETOUTPUTBUF";
|
|
case PAUSE_GETFRAMEBUF: return "GETFRAMEBUF";
|
|
case PAUSE_GETDEPTHBUF: return "GETDEPTHBUF";
|
|
case PAUSE_GETSTENCILBUF: return "GETSTENCILBUF";
|
|
case PAUSE_GETTEX: return "GETTEX";
|
|
case PAUSE_GETCLUT: return "GETCLUT";
|
|
case PAUSE_SETCMDVALUE: return "SETCMDVALUE";
|
|
case PAUSE_FLUSHDRAW: return "FLUSHDRAW";
|
|
default: return "N/A";
|
|
}
|
|
}
|
|
|
|
static void ReleaseRequest(PauseRequest *req) {
|
|
if (retro_atomic_fetch_sub_int(&req->refs, 1) == 1) {
|
|
delete req;
|
|
}
|
|
}
|
|
|
|
static bool CanRunActions() {
|
|
// During CPU stepping, the GE isn't inside a list, so it's safe to run actions then too.
|
|
return coreState == CORE_STEPPING_GE || coreState == CORE_STEPPING_CPU;
|
|
}
|
|
|
|
// Emu thread.
|
|
static void RunPauseAction(PauseRequest &req) {
|
|
DEBUG_LOG(Log::GeDebugger, "RunPauseAction: %s", PauseActionToString(req.action));
|
|
|
|
switch (req.action) {
|
|
case PAUSE_GETOUTPUTBUF:
|
|
req.result = gpu->GetOutputFramebuffer(bufferFrame);
|
|
break;
|
|
|
|
case PAUSE_GETFRAMEBUF:
|
|
req.result = gpu->GetCurrentFramebuffer(bufferFrame, req.bufferType);
|
|
break;
|
|
|
|
case PAUSE_GETDEPTHBUF:
|
|
req.result = gpu->GetCurrentDepthbuffer(bufferDepth);
|
|
break;
|
|
|
|
case PAUSE_GETSTENCILBUF:
|
|
req.result = gpu->GetCurrentStencilbuffer(bufferStencil);
|
|
break;
|
|
|
|
case PAUSE_GETTEX:
|
|
req.result = gpu->GetCurrentTexture(bufferTex, req.bufferLevel, &req.wasFramebuffer);
|
|
break;
|
|
|
|
case PAUSE_GETCLUT:
|
|
req.result = gpu->GetCurrentClut(bufferClut);
|
|
break;
|
|
|
|
case PAUSE_SETCMDVALUE:
|
|
gpu->SetCmdValue(req.setCmdValue);
|
|
break;
|
|
|
|
case PAUSE_FLUSHDRAW:
|
|
gpu->Flush();
|
|
break;
|
|
|
|
default:
|
|
ERROR_LOG(Log::GeDebugger, "Unsupported pause action, forgot to add it to the switch.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Emu thread. Runs the queued requests, or withdraws them all (or the stale ones).
|
|
static void RunPauseRequests(bool withdrawAll) {
|
|
const int epoch = retro_atomic_load_acquire_int(&requestEpoch);
|
|
const bool any = requests.Drain([&](PauseRequest *&&req) {
|
|
if (!withdrawAll && req->epoch == epoch && retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_RUNNING)) {
|
|
RunPauseAction(*req);
|
|
retro_atomic_store_release_int(&req->state, REQUEST_DONE);
|
|
} else {
|
|
retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_WITHDRAWN);
|
|
}
|
|
ReleaseRequest(req);
|
|
});
|
|
if (any) {
|
|
ParkingLotNotify(&requestsWaiting);
|
|
}
|
|
}
|
|
|
|
// Requests an action from the emu thread and waits for it to run. Returns false if stepping ended first
|
|
// (resume, game shutdown), in which case the action is withdrawn.
|
|
static bool RequestPauseAction(PauseRequest *req) {
|
|
_dbg_assert_(strcmp(GetCurrentThreadName(), "EmuThread") != 0);
|
|
|
|
req->epoch = retro_atomic_load_acquire_int(&requestEpoch);
|
|
retro_atomic_fetch_add_int(&requestsWaiting, 1);
|
|
retro_atomic_fetch_add_int(&req->refs, 1);
|
|
requests.Push(req);
|
|
// The emu thread may be stopped, waiting for work.
|
|
Core_WakeCPUThread();
|
|
|
|
// Leaving stepping wakes us through WakeStaleRequests().
|
|
auto finished = [req] {
|
|
const int state = retro_atomic_load_acquire_int(&req->state);
|
|
return state == REQUEST_DONE || state == REQUEST_WITHDRAWN;
|
|
};
|
|
ParkingLotWait(&requestsWaiting, [&] {
|
|
return finished() || !CanRunActions();
|
|
});
|
|
// Nobody will run it. Don't leave it for a later break to run at some unrelated point.
|
|
if (!retro_atomic_cas_int(&req->state, REQUEST_PENDING, REQUEST_WITHDRAWN)) {
|
|
// Already running: it finishes without us.
|
|
ParkingLotWait(&requestsWaiting, finished);
|
|
}
|
|
retro_atomic_fetch_sub_int(&requestsWaiting, 1);
|
|
return retro_atomic_load_acquire_int(&req->state) == REQUEST_DONE;
|
|
}
|
|
|
|
bool ProcessStepping() {
|
|
_dbg_assert_(gpu);
|
|
|
|
if (coreState == CORE_STEPPING_CPU) {
|
|
RunPauseRequests(false);
|
|
return true;
|
|
}
|
|
if (coreState != CORE_STEPPING_GE) {
|
|
// Not stepping any more, don't try.
|
|
return false;
|
|
}
|
|
|
|
if (retro_atomic_load_acquire_int(&continueRequested)) {
|
|
// This is fine, can just mean to run to the next breakpoint/event.
|
|
DEBUG_LOG(Log::GeDebugger, "Continuing...");
|
|
coreState = CORE_RUNNING_GE;
|
|
return false;
|
|
}
|
|
|
|
RunPauseRequests(false);
|
|
return true;
|
|
}
|
|
|
|
// Emu thread.
|
|
bool EnterStepping(CoreState coreState) {
|
|
_dbg_assert_(gpu);
|
|
|
|
if (coreState == CORE_STEPPING_GE) {
|
|
// Already there. Should avoid this happening, I think.
|
|
return true;
|
|
}
|
|
if (coreState != CORE_RUNNING_CPU && coreState != CORE_RUNNING_GE) {
|
|
// ?? Shutting down, don't try to step.
|
|
return false;
|
|
}
|
|
|
|
// StartStepping
|
|
if (lastGState.cmdmem[1] == 0) {
|
|
lastGState = gstate;
|
|
// Play it safe so we don't keep resetting.
|
|
lastGState.cmdmem[1] |= 0x01000000;
|
|
}
|
|
|
|
retro_atomic_store_release_int(&isStepping, 1);
|
|
retro_atomic_fetch_add_int(&stepCounter, 1);
|
|
|
|
// Just to be sure.
|
|
retro_atomic_store_release_int(&continueRequested, 0);
|
|
|
|
::coreState = CORE_STEPPING_GE;
|
|
return true;
|
|
}
|
|
|
|
void ResumeFromStepping() {
|
|
lastGState = gstate;
|
|
retro_atomic_store_release_int(&isStepping, 0);
|
|
retro_atomic_fetch_add_int(&requestEpoch, 1);
|
|
retro_atomic_store_release_int(&continueRequested, 1);
|
|
}
|
|
|
|
// Emu thread.
|
|
void Reset() {
|
|
retro_atomic_store_release_int(&isStepping, 0);
|
|
retro_atomic_store_release_int(&continueRequested, 1);
|
|
retro_atomic_fetch_add_int(&requestEpoch, 1);
|
|
lastGState = {};
|
|
RunPauseRequests(true);
|
|
WakeStaleRequests();
|
|
}
|
|
|
|
void WakeStaleRequests() {
|
|
if (retro_atomic_load_relaxed_int(&requestsWaiting) == 0 || CanRunActions())
|
|
return;
|
|
ParkingLotNotify(&requestsWaiting);
|
|
}
|
|
|
|
bool IsStepping() {
|
|
return retro_atomic_load_acquire_int(&isStepping) != 0;
|
|
}
|
|
|
|
int GetSteppingCounter() {
|
|
return retro_atomic_load_acquire_int(&stepCounter);
|
|
}
|
|
|
|
static bool CanRequest() {
|
|
return IsStepping() || coreState == CORE_STEPPING_CPU;
|
|
}
|
|
|
|
static PauseRequest *NewRequest(PauseAction action) {
|
|
PauseRequest *req = new PauseRequest();
|
|
req->action = action;
|
|
return req;
|
|
}
|
|
|
|
// NOTE: This can't be called on the EmuThread!
|
|
static bool GetBuffer(const GPUDebugBuffer *&buffer, PauseRequest *req, const GPUDebugBuffer &resultBuffer, bool *wasFramebuffer = nullptr) {
|
|
bool result = false;
|
|
if (CanRequest() && RequestPauseAction(req)) {
|
|
buffer = &resultBuffer;
|
|
result = req->result;
|
|
}
|
|
if (wasFramebuffer) {
|
|
*wasFramebuffer = req->wasFramebuffer;
|
|
}
|
|
ReleaseRequest(req);
|
|
return result;
|
|
}
|
|
|
|
bool GPU_GetOutputFramebuffer(const GPUDebugBuffer *&buffer) {
|
|
return GetBuffer(buffer, NewRequest(PAUSE_GETOUTPUTBUF), bufferFrame);
|
|
}
|
|
|
|
bool GPU_GetCurrentFramebuffer(const GPUDebugBuffer *&buffer, GPUDebugFramebufferType type) {
|
|
PauseRequest *req = NewRequest(PAUSE_GETFRAMEBUF);
|
|
req->bufferType = type;
|
|
return GetBuffer(buffer, req, bufferFrame);
|
|
}
|
|
|
|
bool GPU_GetCurrentDepthbuffer(const GPUDebugBuffer *&buffer) {
|
|
return GetBuffer(buffer, NewRequest(PAUSE_GETDEPTHBUF), bufferDepth);
|
|
}
|
|
|
|
bool GPU_GetCurrentStencilbuffer(const GPUDebugBuffer *&buffer) {
|
|
return GetBuffer(buffer, NewRequest(PAUSE_GETSTENCILBUF), bufferStencil);
|
|
}
|
|
|
|
bool GPU_GetCurrentTexture(const GPUDebugBuffer *&buffer, int level, bool *isFramebuffer) {
|
|
PauseRequest *req = NewRequest(PAUSE_GETTEX);
|
|
req->bufferLevel = level;
|
|
return GetBuffer(buffer, req, bufferTex, isFramebuffer);
|
|
}
|
|
|
|
bool GPU_GetCurrentClut(const GPUDebugBuffer *&buffer) {
|
|
return GetBuffer(buffer, NewRequest(PAUSE_GETCLUT), bufferClut);
|
|
}
|
|
|
|
static bool RunRequest(PauseRequest *req) {
|
|
bool result = CanRequest() && RequestPauseAction(req);
|
|
ReleaseRequest(req);
|
|
return result;
|
|
}
|
|
|
|
bool GPU_SetCmdValue(u32 op) {
|
|
PauseRequest *req = NewRequest(PAUSE_SETCMDVALUE);
|
|
req->setCmdValue = op;
|
|
return RunRequest(req);
|
|
}
|
|
|
|
bool GPU_FlushDrawing() {
|
|
return RunRequest(NewRequest(PAUSE_FLUSHDRAW));
|
|
}
|
|
|
|
const GEState &LastState() {
|
|
return lastGState;
|
|
}
|
|
|
|
} // namespace
|