Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions UnleashedRecomp/apple_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ std::filesystem::path GetApplicationSupportDirectory();
/// Returns absolute path suitable for temporary/cached data
std::filesystem::path GetCachesDirectory();

/// Register UIKit lifecycle observers that pause/resume the renderer.
/// SDL's app events are not reliably delivered while the render thread is
/// parked, so the foreground wake-up must come straight from UIKit. No-op on macOS.
void RegisterLifecycleObservers();

/// Open a URL in the system browser
/// Returns true if the URL was opened successfully, false otherwise
bool OpenBrowser(const char* url);
Expand Down
27 changes: 27 additions & 0 deletions UnleashedRecomp/apple_utils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#endif

#include "apple_utils.h"
#include <gpu/video.h>

namespace apple {

Expand Down Expand Up @@ -106,6 +107,32 @@ bool OpenBrowser(const char *url) {
}
}

void RegisterLifecycleObservers() {
#if TARGET_OS_IPHONE
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *) {
Video::HandleApplicationBackgroundState(true);
}];
// Both notifications resume the renderer; the handler is idempotent and
// whichever arrives first wins.
for (NSNotificationName name in @[ UIApplicationWillEnterForegroundNotification,
UIApplicationDidBecomeActiveNotification ]) {
[center addObserverForName:name
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *) {
Video::HandleApplicationBackgroundState(false);
}];
}
});
#endif
}

bool SupportsBCTextures() {
static bool cachedResult = false;
static dispatch_once_t onceToken;
Expand Down
97 changes: 89 additions & 8 deletions UnleashedRecomp/gpu/video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,17 @@ static void DestructTempResources()
static std::thread::id g_presentThreadId = std::this_thread::get_id();
static std::atomic<bool> g_readyForCommands;
static std::atomic<bool> g_appSuspended = false;
static std::atomic<bool> g_renderThreadParked = false;
static bool g_pendingWaitOnSwapChain = true;
static bool g_skipNextSwapChainDrain = false;
static std::atomic<uint32_t> g_resumeFenceGrace = 0;

#ifdef __APPLE__
#include <TargetConditionals.h>
#endif
#if TARGET_OS_IPHONE
#include <CoreFoundation/CoreFoundation.h>
#endif

PPC_FUNC_IMPL(__imp__sub_824ECA00);
PPC_FUNC(sub_824ECA00)
Expand Down Expand Up @@ -1574,12 +1585,23 @@ static void CreateImGuiBackend()

static void CheckSwapChain()
{
// Never touch the swap chain while suspended: the layer's drawables are
// invalid and acquiring would block or fail.
if (g_appSuspended.load(std::memory_order_acquire))
{
g_swapChainValid = false;
return;
}

g_swapChain->setVsyncEnabled(Config::VSync);
g_swapChainValid &= !g_swapChain->needsResize();

if (!g_swapChainValid)
{
Video::WaitForGPU();
if (g_skipNextSwapChainDrain)
g_skipNextSwapChainDrain = false;
else
Video::WaitForGPU();
g_backBuffer->framebuffers.clear();
g_swapChainValid = g_swapChain->resize();
g_needsResize = g_swapChainValid;
Expand Down Expand Up @@ -1675,7 +1697,38 @@ static void BeginCommandList()
commandList->setGraphicsDescriptorSet(g_samplerDescriptorSet.get(), 3);

if (g_appSuspended.load(std::memory_order_relaxed)) {
g_appSuspended.wait(true, std::memory_order_acquire);
g_swapChainValid = false;
#if TARGET_OS_IPHONE
// This is a consistent frame boundary: the executor handshake for the
// frame completed earlier, so the fences WaitForGPU blocks on are real
// in-flight GPU work with bounded completion. Drain here β€” never in the
// lifecycle handler.
g_pendingWaitOnSwapChain = false;
g_dirtyStates.viewport = true;
Video::WaitForGPU();

// The guest render loop runs on the UIKit main thread. While suspended,
// main must neither keep running guest code (the background scene-update
// transaction starves and the watchdog kills the app) nor block on a
// bare wait (the runloop starves and the wake-up notification can never
// be delivered). It has to sit inside the runloop, so the system can
// service its transactions, cleanly suspend the process, and deliver
// the foreground notification that clears the flag on resume.
while (g_appSuspended.load(std::memory_order_acquire))
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, true);
// First frame after resume: force a resize so the stale drawable
// generation is dropped and reacquired. The GPU was already drained on
// the way into the suspend and nothing touched it while idling, so the
// resize must not drain again: at this point a freshly queued command
// list may not have reached the executor yet, and waiting on its fence
// here deadlocks the resume frame.
g_swapChainValid = false;
g_skipNextSwapChainDrain = true;
// Command lists that straddle the suspend can render into drawables
// whose present was skipped; their completion fences may never fire.
// Give the first frames after resume a pass on the pacing fence.
g_resumeFenceGrace.store(NUM_FRAMES, std::memory_order_release);
#endif
}

g_readyForCommands = true;
Expand Down Expand Up @@ -2845,19 +2898,22 @@ static void ProcDrawImGui(const RenderCommand& cmd)
// 3. Loading thread also waits on swap chain.
// 4. Loading thread presents and quits.
// 5. After the loading thread quits, application also presents.
static bool g_pendingWaitOnSwapChain = true;
// (g_pendingWaitOnSwapChain is declared near the suspend state at the top of the file.)

void Video::HandleApplicationBackgroundState(bool isBackgrounded)
{
if (isBackgrounded)
{
g_appSuspended.store(true, std::memory_order_release);
g_readyForCommands.store(false, std::memory_order_release);
g_pendingWaitOnSwapChain = false;
g_swapChainValid = false;
g_dirtyStates.viewport = true;

Video::WaitForGPU();
// Nothing else may happen here. This runs inside a UIKit lifecycle
// callback on the main thread, and any unbounded wait (WaitForGPU
// blocks on a command fence the executor may not signal yet) hangs the
// callback until the scene-update watchdog kills the app. The actual
// drain and swap chain invalidation happen at the render loop's own
// suspend gate, which sits at a consistent frame boundary.
// g_readyForCommands is also deliberately left alone: stopping the
// executor would strand the guest thread on g_executedCommandList.
} else {
g_appSuspended.store(false, std::memory_order_release);
g_appSuspended.notify_all();
Expand Down Expand Up @@ -2902,7 +2958,21 @@ void Video::Present()
g_shouldPrecompilePipelines = false;
}

#if TARGET_OS_IPHONE
// This wait runs on the UIKit main thread. If the executor stalls around a
// suspend, a bare atomic wait would leave the runloop unserviced and the
// background scene-update watchdog kills the app β€” so while suspended,
// service the runloop between checks instead of sleeping blind.
while (!g_executedCommandList.load(std::memory_order_acquire))
{
if (g_appSuspended.load(std::memory_order_acquire))
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, true);
else
g_executedCommandList.wait(false);
}
#else
g_executedCommandList.wait(false);
#endif
g_executedCommandList = false;

if (g_swapChainValid)
Expand All @@ -2925,6 +2995,16 @@ void Video::Present()

if (g_commandListStates[g_frame])
{
uint32_t grace = g_resumeFenceGrace.load(std::memory_order_acquire);
if (grace != 0)
{
// See the resume path: fences of lists that straddled a suspend may
// never signal, so skip the pacing wait for the first frames back.
g_resumeFenceGrace.store(grace - 1, std::memory_order_release);
g_commandListStates[g_frame] = false;
}
else
{
g_frameFenceProfiler.Begin();
g_queue->waitForCommandFence(g_commandFences[g_frame].get());
g_frameFenceProfiler.End();
Expand All @@ -2934,6 +3014,7 @@ void Video::Present()
g_queryPools[g_frame]->queryResults();
const uint64_t *frameTimestamps = g_queryPools[g_frame]->getResults();
g_gpuFrameProfiler.Set(double(frameTimestamps[1] - frameTimestamps[0]) / 1000000.0);
}
}

g_dirtyStates = DirtyStates(true);
Expand Down
2 changes: 2 additions & 0 deletions UnleashedRecomp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ int main(int argc, char *argv[])
os::logger::Init();

#if TARGET_OS_IOS
apple::RegisterLifecycleObservers();

if (!apple::SupportsBCTextures()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, GameWindow::GetTitle(), Localise("System_iOS_UnsupportedGPU_BCTextures").c_str(), GameWindow::s_pWindow);
std::_Exit(1);
Expand Down
1 change: 1 addition & 0 deletions UnleashedRecomp/ui/game_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ int Window_OnSDLEvent(void*, SDL_Event* event)
break;
}

case SDL_APP_WILLENTERFOREGROUND:
case SDL_APP_DIDENTERFOREGROUND: {
Video::HandleApplicationBackgroundState(false);
break;
Expand Down
Loading