Dialog: Follow the firmware's thread priorities in init/shutdown

On a PSP, dialog init and shutdown happen partly at the accessThread
priority and partly at the graphicsThread priority, one phase after the
other. Model that with one helper thread that switches priority per phase,
and let starting it reschedule normally instead of disabling interrupts.

A caller with worse priority than both now sees shutdown complete inside
ShutdownStart, as on hardware. NFL Street 3 (graphics 17, access 19,
caller 111) calls the next InitStart right after ShutdownStart and used to
loop forever on 'A save request is already running' (#19957). The utility
pspautotests, where the caller has better priority, are unchanged.

Also logs the dialog thread priorities at debug level.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-23 19:25:24 -06:00
1 parent 24e44cb5dd
commit 20970d5fad
3 files changed
+50 -12

No files matched your search

+11 -2
View File
@@ -27,6 +27,7 @@
#include "Core/CoreTiming.h"
#include "Core/Dialog/PSPDialog.h"
#include "Core/HLE/sceCtrl.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/HLE/scePower.h"
#include "Core/HLE/sceUtility.h"
#include "Core/MemMapHelpers.h"
@@ -66,6 +67,14 @@ const char *UtilityDialogStatusToString(PSPDialog::DialogStatus status) {
void PSPDialog::InitCommon() {
UpdateCommon();
if (const pspUtilityDialogCommon *common = GetCommonParam()) {
// How these compare with the calling thread decides what the game sees from GetStatus right
// after InitStart/ShutdownStart.
DEBUG_LOG(Log::sceUtility, "%s thread priorities: graphics=%d access=%d font=%d sound=%d (caller %d)",
UtilityDialogTypeToString(DialogType()), common->graphicsThread, common->accessThread,
common->fontThread, common->soundThread, KernelCurThreadPriority());
}
if (GetCommonParam() && GetCommonParam()->language != GetPSPLanguage()) {
WARN_LOG(Log::sceUtility, "Game requested language %d, ignoring and using user language", GetCommonParam()->language);
}
@@ -162,7 +171,7 @@ void PSPDialog::ChangeStatusInit(int delayUs) {
auto params = GetCommonParam();
if (params)
UtilityDialogInitialize(DialogType(), delayUs, params->accessThread);
UtilityDialogInitialize(DialogType(), delayUs, params->accessThread, params->graphicsThread);
else
ChangeStatus(SCE_UTILITY_STATUS_RUNNING, delayUs);
}
@@ -174,7 +183,7 @@ void PSPDialog::ChangeStatusShutdown(int delayUs) {
auto params = GetCommonParam();
if (params && !skipDialogShutdown)
UtilityDialogShutdown(DialogType(), delayUs, params->accessThread);
UtilityDialogShutdown(DialogType(), delayUs, params->accessThread, params->graphicsThread);
else
ChangeStatus(SCE_UTILITY_STATUS_NONE, delayUs);
}
+37 -8
View File
@@ -600,8 +600,21 @@ void __UtilityShutdown() {
delete npSigninDialog;
}
void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int priority) {
// On a PSP, dialog init and shutdown happen partly at the accessThread priority and partly at the
// graphicsThread priority, one phase after the other. One helper thread that switches priority per
// phase reproduces who gets to run when - e.g. a game thread with worse priority than both doesn't
// run again until the dialog has finished starting up or shutting down.
static bool ValidThreadPriority(int priority) {
return priority >= 0x08 && priority <= 0x77;
}
static int PhasePriority(int priority, int fallback) {
return ValidThreadPriority(priority) ? priority : fallback;
}
void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority) {
int partDelay = delayUs / 4;
const int dialogPriority = PhasePriority(graphicsPriority, accessPriority);
const u32_le insts[] = {
// Make sure we don't discard/deadbeef a0.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_S0, MIPS_REG_A0, 0),
@@ -611,12 +624,18 @@ void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int priority)
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A2, MIPS_REG_ZERO, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceSuspendForUser", "sceKernelVolatileMemLock"),
// Loading, at accessThread priority.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
// Setting up the dialog, at graphicsThread priority, then the status goes to RUNNING.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_ZERO, 0),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A1, MIPS_REG_ZERO, dialogPriority),
(u32_le)MIPS_MAKE_SYSCALL("ThreadManForUser", "sceKernelChangeThreadPriority"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
@@ -626,22 +645,34 @@ void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int priority)
};
CleanupDialogThreads(true);
accessThread = new HLEHelperThread("ScePafJob", insts, (uint32_t)ARRAY_SIZE(insts), priority, 0x200);
accessThread = new HLEHelperThread("ScePafJob", insts, (uint32_t)ARRAY_SIZE(insts), accessPriority, 0x200);
accessThread->Start(partDelay, 0);
accessThreadFinished = false;
accessThreadState = "initializing";
}
void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int priority) {
void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority) {
// Break it up so better-priority rescheduling happens.
// The windows aren't this regular, but close.
int partDelay = delayUs / 4;
const int dialogPriority = PhasePriority(graphicsPriority, accessPriority);
const u32_le insts[] = {
// Make sure we don't discard/deadbeef 'em.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_S0, MIPS_REG_A0, 0),
// Tearing down the dialog, at graphicsThread priority.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_ZERO, 0),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A1, MIPS_REG_ZERO, dialogPriority),
(u32_le)MIPS_MAKE_SYSCALL("ThreadManForUser", "sceKernelChangeThreadPriority"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
// Cleaning up at accessThread priority, then the status goes to NONE.
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_ZERO, 0),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A1, MIPS_REG_ZERO, accessPriority),
(u32_le)MIPS_MAKE_SYSCALL("ThreadManForUser", "sceKernelChangeThreadPriority"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityWorkUs"),
(u32_le)MIPS_MAKE_ORI(MIPS_REG_A0, MIPS_REG_S0, 0),
@@ -652,15 +683,13 @@ void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int priority) {
(u32_le)MIPS_MAKE_SYSCALL("sceUtility", "__UtilityFinishDialog"),
};
// Starting the thread reschedules normally, so a caller with worse priority than both phases sees
// NONE by the time ShutdownStart returns.
CleanupDialogThreads(true);
bool prevInterrupts = __InterruptsEnabled();
__DisableInterrupts();
accessThread = new HLEHelperThread("ScePafJob", insts, (uint32_t)ARRAY_SIZE(insts), priority, 0x200);
accessThread = new HLEHelperThread("ScePafJob", insts, (uint32_t)ARRAY_SIZE(insts), accessPriority, 0x200);
accessThread->Start(partDelay, 0);
accessThreadFinished = false;
accessThreadState = "shutting down";
if (prevInterrupts)
__EnableInterrupts();
}
static int UtilityWorkUs(int us) {
+2 -2
View File
@@ -123,8 +123,8 @@ void __UtilityInit();
void __UtilityDoState(PointerWrap &p);
void __UtilityShutdown();
void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int priority);
void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int priority);
void UtilityDialogInitialize(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority);
void UtilityDialogShutdown(UtilityDialogType type, int delayUs, int accessPriority, int graphicsPriority);
typedef void (*ModuleLoadCallback)(int state, u32 loadAddr, u32 loadSize);