Skip to content

feat(libSceNpManager): register NP state callbacks and report signed-out from sceNpCheckCallback - #6121

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
fdfawef324:claude/mc-np-state-callbacks
Oct 18, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
fdfawef324:claude/mc-np-state-callbacks

Conversation

@fdfawef324

Copy link
Copy Markdown
Contributor

What

#1277 reports that Minecraft Preview (PPSA19639) needs this for stop 2: the title registers an NP state callback and expects sceNpCheckCallback to deliver SIGNED_OUT once. This PR is the NpManager part of that stop. On main, sceNpRegisterStateCallback and sceNpRegisterStateCallbackA store nothing (the A form always returns 1), both unregister calls always return 0, and sceNpCheckCallback does nothing.

Registration follows shadPS4 np_manager.cpp @15abf68c (RegisterStateCallbackA and UnregisterStateCallbackAById L857-L901, the legacy pair L981-L1005, ORBIS_NP_STATE_CALLBACK_MAX = 8 at L36):

  • sceNpRegisterStateCallbackA(cb, ud) keeps up to 8 callbacks and returns the slot plus 1. A null callback returns INVALID_ARGUMENT (0x80550003), a callback that is already registered CALLBACK_ALREADY_REGISTERED (0x80550008), and a ninth one CALLBACK_MAX (0x8055001D, np_error.h L38).
  • sceNpUnregisterStateCallbackA(id) returns INVALID_ARGUMENT for an id outside 1..8 and CALLBACK_NOT_REGISTERED (0x80550009) for an empty slot.
  • sceNpRegisterStateCallback(cb, ud) is the legacy form void(int userId, uint32_t state, NpId* npId, void* ud) with one slot: a null callback is INVALID_ARGUMENT, a second registration CALLBACK_ALREADY_REGISTERED. sceNpUnregisterStateCallback with nothing registered returns CALLBACK_NOT_REGISTERED.
  • Both callback types are declared with APS5_VABI.

Delivery:

  • Each registration is marked pending.
  • sceNpCheckCallback copies the registrations and clears the pending flags under the mutex. After releasing it, on the calling thread, it calls the A form as cb(0x10000000, SIGNED_OUT = 1, ud) and the legacy form as cb(0x10000000, 1, nullptr, ud). 0x10000000 is USER_SERVICE_INITIAL_USER_ID from prx/libSceUserService/UserService.hpp.
  • The A form follows KytyPS5 network.cpp @9e41d832: NpRegisterStateCallbackA (L4021-L4034) marks the registration pending, and NpCheckCallback (L3959-L3974) delivers it once. KytyPS5 never calls the legacy callback: its NpRegisterStateCallback and NpUnregisterStateCallback (L4009-L4019) do nothing.
  • The legacy call with a null NpId follows shadPS4, which sets has_np_id only for SignedIn (L823) and passes nullptr otherwise (L938-L940).
  • A registration removed before the next check is not called. Because the callbacks run after the mutex is released, a callback can unregister itself. A callback that one thread is already delivering is still called once if another thread unregisters it meanwhile, as with shadPS4's copy-then-dispatch.
  • The reachability callback is still not delivered (its debt entry is unchanged). Unlike shadPS4, whose DispatchPendingNpStateCallbacks also calls it once with UNAVAILABLE for the same event (L922-L931, L959-L961), it gets no initial call.

shadPS4 queues SIGNED_OUT from the user service login event and delivers it from sceNpCheckCallback (DispatchPendingNpStateCallbacks L903-L962). That difference, the reachability difference, the unregister race and the fact that the console's timing and errors are not measured are recorded in docs/dev/debt/libSceNpManager.md.

Test: GuestNpManager.cpp (guest_np_manager) now also checks both forms with APS5_VABI callbacks that record userId, state, npId and userdata:

  • A form: a null callback is rejected and the returned id is in 1..8. A duplicate returns 0x80550008. Registering calls nothing. An aborting reachability callback is registered around the first check, which calls the state callback once as (0x10000000, 1, ud) and never calls the reachability callback. A second check calls nothing. The 8 ids are distinct. With 8 slots full, the next registration returns 0x8055001D, and the next check calls each of the 7 new callbacks once. Unregistering ids 0 and 9 returns 0x80550003. After unregistering one id in the middle, each of the other 7 callbacks still returns 0x80550008, the removed callback registers again into the same id, and only it is called by the next check. Unregistering the same id twice returns 0x80550009 the second time. A callback that is registered and then unregistered before the next check is not called. A callback that calls sceNpUnregisterStateCallbackA on its own id gets 0, and its id is then empty.
  • Legacy form: unregistering with nothing registered returns 0x80550009, a null callback is rejected, and a duplicate returns 0x80550008. The first check makes one call with a null npId, and a second check makes none. Unregistering returns 0, and a second unregister returns 0x80550009.
  • guest_np_reachability still passes.

Need on current main: the only evidence for stop 2 is the #1277 reporter's local patch on main d4c196c2, and a 2026-10-14 comment there notes that stop 2 is not covered by #5133. Minecraft (PPSA17221), which shares the engine, is reported to run on main without this change. The change affects every title that registers a state callback: it now gets one SIGNED_OUT call that it never got before. I have not run either title, so this PR does not confirm the stop on current main. @ddrsilva, or anyone who runs PPSA17221: could you run this branch with the Preview and with PPSA17221 before merge? I will add the result to Tested.

Tested

Windows Server 2025 (10.0.26100), WinLibs MinGW-w64 GCC 15.2.0, Ninja, Release, BUILD_TESTING=ON, on main 922abe6.

Not tested: Linux, macOS, titles.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA19639 Minecraft Preview (PPSA19639) #1277
  • GPUs tested: n/a (no AGC, shader recompiler or VideoOut change)
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

…out from sceNpCheckCallback

sceNpRegisterStateCallback and sceNpRegisterStateCallbackA stored nothing,
the unregister calls always succeeded and sceNpCheckCallback did nothing,
so a title that waits for its NP state callback never learned the user's
state. boykopovar#1277 reports that Minecraft Preview (PPSA19639) needs the callback
(stop 2).

sceNpRegisterStateCallbackA now keeps up to 8 callbacks and returns the
slot plus 1. A null callback returns INVALID_ARGUMENT, a callback that is
already registered CALLBACK_ALREADY_REGISTERED and a ninth one
CALLBACK_MAX (0x8055001D). sceNpUnregisterStateCallbackA returns
INVALID_ARGUMENT for an id outside 1..8 and CALLBACK_NOT_REGISTERED for an
empty slot. The legacy sceNpRegisterStateCallback keeps one callback with
the same errors. The rules follow shadPS4
src/core/libraries/np/np_manager.cpp @15abf68c9b52 (RegisterStateCallbackA
and UnregisterStateCallbackAById L857-L901, the legacy pair L981-L1005,
ORBIS_NP_STATE_CALLBACK_MAX L36) and np_error.h L38.

Each registration is marked pending. sceNpCheckCallback takes the pending
registrations under the lock and calls them after releasing it, on the
calling thread, with SIGNED_OUT for the initial user 0x10000000. For the A
form this follows KytyPS5 src/libs/network.cpp @9e41d8320a27
(NpRegisterStateCallbackA L4021-L4034 marks the registration pending,
NpCheckCallback L3959-L3974 delivers it once). KytyPS5 never calls the
legacy callback (NpRegisterStateCallback and NpUnregisterStateCallback,
L4009-L4019, do nothing). The legacy callback is called as
(user, SIGNED_OUT, nullptr, userdata), as in shadPS4, which passes an NpId
only for SIGNED_IN (L823, L938-L940).

shadPS4 queues SIGNED_OUT from the user service login event instead
(DispatchPendingNpStateCallbacks L903-L962), and the same event calls the
reachability callback once with UNAVAILABLE (L922-L931, L959-L961). The
reachability callback is still not called here. These differences and the
unmeasured console behaviour are recorded in
docs/dev/debt/libSceNpManager.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 6044 core/libs/tests/GuestNpManager.cpp
PR 6977 core/libs/prx/libSceNpManager/Export.cpp
docs/dev/debt/libSceNpManager.md

@isnisk

isnisk commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Checked on Linux: Ubuntu 26.04 (WSL2), GCC 15.2.0, Release, this head (9f96444c4) merged onto main 489466d3f (clean merge).

  • With the change: guest_np_manager, guest_np_reachability and the other 7 guest_np_* tests pass, 3 rounds; no warnings from the changed files.
  • Without it (main's Export.cpp, the new test kept): guest_np_manager aborts 3 of 3 with NpManager: duplicate state callback, as on Windows.
  • check_conventions.py --base origin/main passes.

Not checked: macOS, the titles.

AI-assisted: yes (Claude Code).

@ddrsilva

Copy link
Copy Markdown

Preview check for head 9f96444c4a511d64fb7efd184dafe12de3fa66b9: Windows 11 Pro 26200, WinLibs MinGW-w64 GCC 15.2, Minecraft Preview 1.21.40.22 (PPSA19639), RTX 2080 Ti, 1920x1080 / 32 chunks.

I built the complete libSceNpManager/Export.cpp from this head unchanged, using the PR's headers, and substituted that module in an isolated copy of my existing working Minecraft integration. Preview reached its title screen, loaded the saved world and entered rendered gameplay without a crash or NP-related stop. The loaded-module path/hash were checked. A read-only snapshot of the PR module's state showed one registered A-form guest callback with its pending flag cleared, consistent with it being processed by sceNpCheckCallback; this is not a callback entry/return trace.

The parent control also entered gameplay. I built the parent revision's complete NP module (922abe6a4) and ran it with the same other 120 modules, fixture and launch flags. Therefore I cannot reproduce the original stop 2 in this configuration or claim that this PR newly unblocks Preview. The useful result is no observed Preview world-entry regression with the proposed callback behavior.

The PR's GuestNpManager and GuestNpReachability tests pass three runs each. Keeping the new tests and using the parent's module fails GuestNpManager with NpManager: duplicate state callback (0xc0000409); reachability still passes.

@adriadam10 adriadam10 self-assigned this Oct 16, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the state callback registration (8 slots, legacy single slot, error codes vs shadPS4), delivery from sceNpCheckCallback outside the mutex, self-unregistering callbacks and the debt entry. guest_np_manager passes with the change and fails without it; scoped ctest (36 tests) green on this head merged onto main. Third-party runs (Linux, Preview) noted in the thread. Merging.

@MarcZX10

Copy link
Copy Markdown
Contributor

I have a small offline-profile follow-up for issue #1794 that adds sceNpGetAccountIdA and aligns sceUserServiceGetNpAccountId to a fixed local account ID. tools/pr_overlap.py reports a merge conflict in libSceNpManager/Export.cpp at the shared include block, although the functions are otherwise distinct. Do you have a preferred merge order? I can declare a dependency and rebase after yours lands.

@MarcZX10

Copy link
Copy Markdown
Contributor

I adjusted the include placement; the latest pr_overlap.py run no longer reports a conflict with #6121. No dependency or merge order is needed for the current diff.

@adriadam10
adriadam10 merged commit b5ab101 into boykopovar:main Oct 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants