Skip to content

Commit dd59d0e

Browse files
committed
Fix HOME-exit hang by handing ProcUI lifecycle to SDL2-wuhb
1 parent cda2950 commit dd59d0e

8 files changed

Lines changed: 169 additions & 53 deletions

File tree

src/app/App.cpp

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ extern void elogf(const char* fmt, ...);
1919
#include <SDL2/SDL.h>
2020
#include <whb/proc.h>
2121
#include <proc_ui/procui.h>
22+
#include <coreinit/thread.h>
23+
#include <coreinit/time.h>
2224
#include <SDL2/SDL_ttf.h>
2325
#include <SDL2/SDL_image.h>
2426

@@ -32,6 +34,11 @@ extern "C" {
3234
App::App() = default;
3335

3436
App::~App() {
37+
// Standard SDL2-on-Wii-U teardown order. SDL_Quit() internally drives
38+
// SYSLaunchMenu + ProcUIProcessMessages drain + ProcUIShutdown via its
39+
// WIIU_VideoQuit handler (because we let SDL_Init claim ProcUI in
40+
// main() by NOT calling WHBProcInit). No custom ProcUI bookkeeping
41+
// needed on our side.
3542
elog("~App: DownloadQueue stop");
3643
DownloadQueue::get().stop();
3744
elog("~App: clearing screens");
@@ -48,7 +55,7 @@ App::~App() {
4855
IMG_Quit();
4956
elog("~App: TTF_Quit");
5057
TTF_Quit();
51-
elog("~App: SDL_Quit");
58+
elog("~App: SDL_Quit (drains ProcUI internally)");
5259
SDL_Quit();
5360
elog("~App: done");
5461
}
@@ -60,10 +67,16 @@ bool App::init() {
6067
// Without these calls, Cemu's emulated socket layer never finishes
6168
// setup and curl connect() hangs. The previous BUILD_HW gate broke
6269
// cemu mode entirely (app stuck on "Loading repos...").
63-
nn::ac::Initialize();
64-
nn::ac::Connect();
70+
{
71+
nn::Result r = nn::ac::Initialize();
72+
elogf("Network: nn::ac::Initialize -> %s", r.IsSuccess() ? "ok" : "FAILED");
73+
}
74+
{
75+
nn::Result r = nn::ac::Connect();
76+
elogf("Network: nn::ac::Connect -> %s", r.IsSuccess() ? "ok" : "FAILED");
77+
}
6578
socket_lib_init();
66-
elog("Network ready");
79+
elog("Network: socket_lib_init done, network ready");
6780
#endif
6881

6982
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
@@ -130,7 +143,11 @@ bool App::init() {
130143
elog("mkdir done");
131144
Logger::get().init(Paths::modstoreBase() + "/app.log");
132145
elog("logger init done");
133-
LOG_INFO("App started");
146+
#ifdef APP_VERSION
147+
LOG_INFO("App started (CoffeeShop %s)", APP_VERSION);
148+
#else
149+
LOG_INFO("App started (no version define)");
150+
#endif
134151
elog("before pushScreen");
135152
pushScreen(std::make_unique<MainLayout>(this));
136153
elog("after pushScreen");
@@ -164,62 +181,67 @@ void App::run() {
164181
m_running = true;
165182
int pruneCounter = 0;
166183
int frameNum = 0;
167-
elogf("App::run entering -- screens=%zu running=%d procRunning=%d",
168-
m_screens.size(), (int)m_running,
169-
#ifdef __WUT__
170-
(int)WHBProcIsRunning()
171-
#else
172-
1
173-
#endif
174-
);
184+
elogf("App::run entering -- screens=%zu running=%d", m_screens.size(), (int)m_running);
185+
// NOTE: We deliberately do NOT call WHBProcIsRunning() here -- it would
186+
// run ProcUIProcessMessages and then ProcUIShutdown() because WHB's
187+
// sRunning flag is false (we never called WHBProcInit). That would tear
188+
// down ProcUI before we ever rendered a frame. SDL drives ProcUI for
189+
// us inside SDL_PollEvent below.
175190

176-
while (m_running && !m_screens.empty() && WHBProcIsRunning()) {
177-
if (frameNum < 10 || frameNum % 60 == 0) {
178-
elogf("frame %d begin", frameNum);
179-
}
191+
// SDL2-wuhb owns the ProcUI lifecycle (we don't call WHBProcInit, so
192+
// SDL_Init claims it via ProcUIInitEx and sets handleProcUI=TRUE on
193+
// its side). SDL drives ProcUI from WIIU_PumpEvents (called by
194+
// SDL_PollEvent below) and fires SDL_QUIT when EXITING is received.
195+
// We must NOT use WHBProcIsRunning() as a loop guard: WHB's sRunning
196+
// flag is false because we never called WHBProcInit, and the first
197+
// WHBProcIsRunning() call would call ProcUIShutdown() and return
198+
// false, exiting the loop with 0 frames. Use SDL_QUIT (set via
199+
// m_running=false) as the canonical exit trigger instead.
200+
while (m_running && !m_screens.empty()) {
201+
// Log only the first 10 frames to confirm the loop started cleanly.
202+
// Periodic per-second pulses were diagnostic for the HOME-exit hang
203+
// and are no longer needed; they'd just bloat early.log.
204+
if (frameNum < 10) elogf("frame %d begin", frameNum);
180205
SDL_Event event;
181206
while (SDL_PollEvent(&event)) {
182207
if (event.type == SDL_QUIT) {
183208
elog("got SDL_QUIT");
184209
m_running = false;
185210
}
186211
}
187-
if (frameNum < 5) elog(" before update");
188212
update();
189-
if (frameNum < 5) elog(" after update");
190213
render();
191-
if (frameNum < 5) elogf(" after render (sdl_err='%s')", SDL_GetError());
192214
TextCache::get().tick();
193215
if (++pruneCounter > 300) {
194216
TextCache::get().prune();
195217
pruneCounter = 0;
196-
elogf("frame %d: pruned text cache", frameNum);
197218
}
198219
frameNum++;
199220
}
200-
elogf("App::run loop exited -- frames=%d running=%d screens=%zu procRunning=%d",
201-
frameNum, (int)m_running, m_screens.size(),
202-
#ifdef __WUT__
203-
(int)WHBProcIsRunning()
204-
#else
205-
1
206-
#endif
207-
);
208-
#ifdef __WUT__
209-
// Drain ProcUI until Aroma confirms exit - required for WHBProcShutdown to work
210-
while (WHBProcIsRunning()) {}
211-
#endif
221+
elogf("App::run loop exited -- frames=%d running=%d screens=%zu exiting=%d",
222+
frameNum, (int)m_running, m_screens.size(), (int)m_exiting);
223+
// No ProcUI drain here. SDL_Quit's WIIU_VideoQuit handles the exit
224+
// transition: it calls SYSLaunchMenu, then loops on
225+
// ProcUIProcessMessages(TRUE) handling RELEASE_FOREGROUND and exiting
226+
// when EXITING is received, then calls ProcUIShutdown. We just need
227+
// to return from run() so main() can let the App destructor (and
228+
// therefore SDL_Quit) run.
212229
}
213230

214231
void App::quit() {
232+
elog("App::quit called -- setting m_running=false (no SYSLaunchMenu)");
215233
m_running = false;
216234
}
217235
void App::startExit() {
236+
// Just stop the main loop. SDL2-wuhb is the single owner of the
237+
// ProcUI/SYSLaunchMenu handshake: SDL_Quit's WIIU_VideoQuit detects
238+
// !exitingProcUI and calls SYSLaunchMenu + drains ProcUI itself.
239+
// Calling SYSLaunchMenu here too would issue it twice in flight
240+
// (once now, once during SDL_Quit) and reproduce the original
241+
// "Wii U Menu loading" hang. See Codex review 2026-05-..
242+
elog("App::startExit called -- m_running=false, SDL_Quit will own SYSLaunchMenu");
218243
m_exiting = true;
219244
m_running = false;
220-
#ifdef __WUT__
221-
SYSLaunchMenu();
222-
#endif
223245
}
224246

225247
void App::pushScreen(std::unique_ptr<Screen> screen) {
@@ -228,11 +250,15 @@ void App::pushScreen(std::unique_ptr<Screen> screen) {
228250
}
229251

230252
void App::popScreen() {
253+
elogf("App::popScreen -- stack=%zu before", m_screens.size());
231254
if (!m_screens.empty()) {
232255
m_screens.back()->onExit();
233256
m_screens.pop_back();
234257
}
235-
if (m_screens.empty()) m_running = false;
258+
if (m_screens.empty()) {
259+
elog("App::popScreen -- stack empty, setting m_running=false (no SYSLaunchMenu)");
260+
m_running = false;
261+
}
236262
}
237263

238264
void App::update() {

src/audio/AudioManager.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ void AudioManager::init() {
3939
}
4040

4141
void AudioManager::shutdown() {
42-
if (!m_initialized) return;
42+
if (!m_initialized) {
43+
LOG_INFO("AudioManager::shutdown: already shut down, skipping");
44+
return;
45+
}
46+
LOG_INFO("AudioManager::shutdown begin");
4347
stopMusic();
4448
for (auto& [id, chunk] : m_sounds) Mix_FreeChunk(chunk);
4549
m_sounds.clear();
@@ -50,6 +54,7 @@ void AudioManager::shutdown() {
5054
Mix_CloseAudio();
5155
Mix_Quit();
5256
m_initialized = false;
57+
LOG_INFO("AudioManager::shutdown done");
5358
}
5459

5560
void AudioManager::playSound(SoundId id) {

src/main.cpp

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ extern "C" {
2222
}
2323
#endif
2424

25+
#include "util/LogClock.h"
26+
2527
static const char* g_elog_path = nullptr;
28+
2629
void elog(const char* msg) {
27-
WHBLogPrintf("[elog] %s", msg);
30+
double t = LogClock::elapsedSec();
31+
WHBLogPrintf("[elog %8.3f] %s", t, msg);
2832
if (g_elog_path) {
2933
FILE* f = fopen(g_elog_path, "a");
3034
if (f) {
31-
fprintf(f, "%s\n", msg);
35+
fprintf(f, "[%8.3f] %s\n", t, msg);
3236
fclose(f);
3337
}
3438
}
@@ -46,6 +50,11 @@ void elogf(const char* fmt, ...) {
4650
}
4751

4852
int main(int argc, char** argv) {
53+
// Set the log epoch as the very first thing so every subsequent timestamp
54+
// is relative to "process started". Anything that fired before this point
55+
// is pre-runtime and not interesting to log.
56+
LogClock::init();
57+
4958
// Diagnostic: prove main() was called before ANY WHB/SDL init.
5059
{
5160
FILE* probe = fopen("/vol/external01/wiiu/apps/coffeeshop/main_called.txt", "w");
@@ -55,7 +64,15 @@ int main(int argc, char** argv) {
5564
}
5665
}
5766

58-
WHBProcInit();
67+
// NOTE: We do NOT call WHBProcInit() here. SDL_Init() will detect that
68+
// ProcUI isn't running yet and call ProcUIInitEx() itself, claiming
69+
// ownership of the ProcUI lifecycle. With handleProcUI=TRUE on its side,
70+
// SDL drives ProcUI from PumpEvents (sending SDL_QUIT on EXITING) and
71+
// properly drains it on SDL_Quit (SYSLaunchMenu + ProcUIProcessMessages
72+
// loop). If we initialised ProcUI first, SDL would back off and we'd
73+
// be stuck handling the entire state machine ourselves -- which is
74+
// exactly the rabbit hole that produced the HOME-exit hang. See
75+
// SDL_wiiuvideo.c:WIIU_VideoInit / WIIU_VideoQuit.
5976
WHBLogUdpInit();
6077

6178
#ifdef __WUT__
@@ -84,6 +101,11 @@ int main(int argc, char** argv) {
84101
}
85102
}
86103
elog("START");
104+
#ifdef APP_VERSION
105+
elogf("CoffeeShop %s", APP_VERSION);
106+
#else
107+
elog("CoffeeShop (no version define)");
108+
#endif
87109
elog(Paths::sdMounted ? "SD mounted" : "SD failed");
88110

89111
// Crash dump catcher: writes a minimal report on SIGSEGV/etc so users
@@ -131,9 +153,9 @@ int main(int argc, char** argv) {
131153
socket_lib_finish();
132154
elog("ac::Finalize");
133155
nn::ac::Finalize();
156+
// No WHBProcShutdown: SDL_Quit's WIIU_VideoQuit already ran SYSLaunchMenu,
157+
// drained ProcUI, and called ProcUIShutdown via its handleProcUI path.
158+
// Calling WHBProcShutdown again would either no-op or interfere.
134159
#endif
135-
// elog uses open+write+close per call; nothing to fclose at exit.
136-
// WHBProcShutdown intentionally omitted (CLAUDE.md): App::run drains
137-
// ProcUI and SYSLaunchMenu hands control back to the OS.
138160
return 0;
139161
}

src/net/DownloadQueue.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ void DownloadQueue::start() {
6363
}
6464

6565
void DownloadQueue::stop() {
66+
LOG_INFO("DownloadQueue: stop() called, signalling %d worker(s)", (int)m_workers.size());
6667
{
6768
std::lock_guard<std::mutex> lock(m_mutex);
6869
for (auto& j : m_jobs) j.cancel = true;
@@ -71,19 +72,22 @@ void DownloadQueue::stop() {
7172
m_cv.notify_all();
7273
for (auto& w : m_workers) if (w.joinable()) w.join();
7374
m_workers.clear();
75+
LOG_INFO("DownloadQueue: stop() done");
7476
}
7577

7678
void DownloadQueue::cancelJob(int index) {
7779
std::lock_guard<std::mutex> lock(m_mutex);
7880
if (index >= 0 && index < (int)m_jobs.size()) {
7981
auto& j = m_jobs[index];
8082
if (j.state == DownloadJob::State::Pending) {
83+
LOG_INFO("DownloadQueue: cancelled pending job '%s'", j.mod.name.c_str());
8184
j.state = DownloadJob::State::Error;
8285
j.error = "Cancelled";
8386
j.hasFinishedAt = true;
8487
j.finishedAt = std::chrono::steady_clock::now();
8588
} else if (j.state == DownloadJob::State::Downloading ||
8689
j.state == DownloadJob::State::Extracting) {
90+
LOG_INFO("DownloadQueue: cancel flag set on active job '%s'", j.mod.name.c_str());
8791
j.cancel = true;
8892
}
8993
}
@@ -162,8 +166,10 @@ void DownloadQueue::workerLoop() {
162166
jobCancel = &m_jobs[pendingIdx].cancel;
163167
}
164168

169+
LOG_INFO("DownloadQueue: starting job '%s' for title %s", jobMod.name.c_str(), jobTitleId.c_str());
165170
processJob(pendingIdx, jobMod, jobTitleId, jobCancel);
166171
}
172+
LOG_INFO("DownloadQueue: worker exiting");
167173
}
168174

169175
void DownloadQueue::processJob(int idx, const Mod& mod,

src/ui/DetailScreen.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "ui/RegionSelectScreen.h"
33
#include "mods/InstallHelper.h"
44
#include "util/TextCache.h"
5+
#include "util/Logger.h"
56
#include "mods/InstallChecker.h"
67
#include "mods/InstalledScanner.h"
78
#include "net/DownloadQueue.h"
@@ -38,6 +39,7 @@ void DetailScreen::handleInput(const Input& input) {
3839

3940
if (m_confirmUninstall) {
4041
if (input.a) {
42+
LOG_INFO("DetailScreen: confirm uninstall '%s'", m_mod.id.c_str());
4143
auto mods = InstalledScanner::scan();
4244
for (auto& inst : mods) {
4345
if (inst.id == m_mod.id) {
@@ -47,7 +49,10 @@ void DetailScreen::handleInput(const Input& input) {
4749
}
4850
m_app->popScreen();
4951
}
50-
if (input.b) m_confirmUninstall = false;
52+
if (input.b) {
53+
LOG_INFO("DetailScreen: cancel uninstall '%s'", m_mod.id.c_str());
54+
m_confirmUninstall = false;
55+
}
5156
return;
5257
}
5358

@@ -58,18 +63,24 @@ void DetailScreen::handleInput(const Input& input) {
5863
if (!m_loaded) return;
5964

6065
if (input.y && m_installStatus.installed) {
66+
LOG_INFO("DetailScreen: prompt uninstall '%s'", m_mod.id.c_str());
6167
m_confirmUninstall = true;
6268
return;
6369
}
6470

6571
if (input.a && !m_titleIds.empty()) {
66-
if (m_installStatus.installed && !m_installStatus.updateAvail) return;
72+
if (m_installStatus.installed && !m_installStatus.updateAvail) {
73+
LOG_INFO("DetailScreen: A pressed but '%s' already installed at latest version", m_mod.id.c_str());
74+
return;
75+
}
6776
auto entries = InstallHelper::detectInstalled(m_titleIds);
6877
if (entries.size() == 1) {
78+
LOG_INFO("DetailScreen: install '%s' to title %s", m_mod.id.c_str(), entries[0].id.c_str());
6979
AudioManager::get().playSound(SoundId::DownloadStart);
7080
DownloadQueue::get().enqueue(m_mod, entries[0].id);
7181
m_app->popScreen();
7282
} else {
83+
LOG_INFO("DetailScreen: %zu title regions found, opening RegionSelect for '%s'", entries.size(), m_mod.id.c_str());
7384
m_app->pushScreen(std::make_unique<RegionSelectScreen>(
7485
m_app, m_mod, entries));
7586
}

0 commit comments

Comments
 (0)