Skip to content

feat(libSceUserService): implement event callback delivery - #14848

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
xdCloudy:fix/libSceUserService-event-callbacks
Nov 6, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
xdCloudy:fix/libSceUserService-event-callbacks

Conversation

@xdCloudy

@xdCloudy xdCloudy commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

What

Implement event callback registration, asynchronous delivery and unregistration. One host worker invokes callbacks outside service locks; controller/presentation callers only enqueue events. Removed registrations remain in drain accounting until their active call finishes, including overlapping unregister and termination. Delivery is serialized, preventing mutual cross-callback drain waits. Self-unregister and removal after snapshot remain supported.

Firmware 13.60 libSceUserService.sprx evidence: registration 0x34d0 establishes unique pairs, null-success and the initialization check; unregister 0x35f0 establishes null rejection and absent-pair success. Initialization helper 0x1930 creates SceUserServiceEvent at 0x2380, which drains messages through 0x1e10. The latter requests 8-byte events and invokes the callback with the event pointer and stored user data, releasing the callback-list lock first. Shell wrappers are at 0x28a0 and 0x24d0. The debt entry records the module digest and separates these static observations from host snapshots, drain accounting and scheduling choices. Console behavior was not measured.

The shell callback table, IPMI backend and event sources beyond login/logout remain unavailable. This implementation is independent of the settings, group, user-name and debt-formatting PRs; shared source/debt files may need merge-order reconciliation.

Tested

Windows x86_64, WinLibs GCC, CMake/Ninja Release: built libSceUserService and guest_user_service_lifecycle_tests. Lifecycle CTest passed for 30 consecutive runs. Coverage includes two synchronized notification producers with cross-unregister, off-caller-thread blocking delivery, self-removal, removal after snapshot, inactive registrations retained during concurrent unregister/termination, duplicate/null registrations and calls before initialization. Drain tests synchronize on registration/lifecycle state rather than a sleep. The test compiles the production source directly to inspect that private state. Public-header audit and conventions passed. New-head hosted checks pending. No console or new Linux runtime run.

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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Progress report (bbc1388 → 4977956)

📈 System libraries: 30.07% (+0.01%, +5 implemented functions)

✅ 5 implemented
Library Name
libSceUserService sceUserServiceGetEventVsh
libSceUserService sceUserServiceInitializeForShellCore
libSceUserService sceUserServiceRegisterEventCallback
libSceUserService sceUserServiceTerminateForShellCore
libSceUserService sceUserServiceUnregisterEventCallback

Changed libraries (implemented exports)

Library Before After Change
libSceUserService 40/695 (5.76%) 45/695 (6.47%) +0.72%

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — changes requested

The callback lifetime implementation is internally coherent, but the firmware behavior claims lack the source locations required by the repository’s reverse-engineering guidance.

What I ran

Reviewed head 8f848048b1c361998cb6888324181a8d5cad4e4e against base e0caf1360035286bd22b9f2a3030e97b880ffb28; the PR itself merges cleanly against that base. Environment: Debian 12, Linux 6.1, Python 3.11.2, GCC 12.2. No local build or CTest was run. The author reports a Windows build and lifecycle test pass; those results were not independently reproduced.

git diff --check e0caf1360035286bd22b9f2a3030e97b880ffb28...8f848048b1c361998cb6888324181a8d5cad4e4e
python3 tools/pr_precheck.py --repo boykopovar/AnyPS5 --pr 14848
python3 tools/check_conventions.py --base e0caf1360035286bd22b9f2a3030e97b880ffb28 --head 8f848048b1c361998cb6888324181a8d5cad4e4e --title 'fix(libSceUserService): make event callbacks reentrant' --body /tmp/anyps5-pr-precheck/boykopovar-AnyPS5-pr-14848-8f848048b1c3/pr-body.md
python3 tools/pr_abi_audit.py --base e0caf1360035286bd22b9f2a3030e97b880ffb28 --head 8f848048b1c361998cb6888324181a8d5cad4e4e --json
python3 tools/pr_stack_check.py 14848 --repo boykopovar/AnyPS5 --output-dir /tmp/anyps5-pr-precheck/boykopovar-AnyPS5-pr-14848-8f848048b1c3/stack
python3 tools/pr_test_quality.py --root /marc/AnyPS5 --json core/libs/tests/GuestUserServiceLifecycle.cpp
git grep -n -E 'GuestUserServiceLifecycle\.cpp|guest_user_service_lifecycle' 8f848048b1c361998cb6888324181a8d5cad4e4e -- '*CMakeLists.txt' '*.cmake'
gh pr checks 14848 --repo boykopovar/AnyPS5

Results

Check Result Note
Diff and conventions passed No whitespace errors; conventions passed.
Export audit passed No lexical ABI findings.
Test registration passed Lifecycle test is registered in CMake.
Stack failed Declared dependency heads are not included; two dependency PRs conflict with current main.
Hosted checks not run Windows remains pending; portability and report passed.
Build and CTest here not run Author reports a Windows build and focused test pass.
Precheck and hosted-check details

The precheck confirmed the head and base were unchanged. Its isolated snapshot lacked the workspace-only ABI and test-quality helper scripts. The standalone ABI audit found zero findings. The test-quality heuristic warned that the C++ test might be unregistered; direct CMake inspection confirmed registration at core/libs/prx/libSceUserService/CMakeLists.txt:31-36.

The stack report says #14848 itself merges cleanly against the current base, but its declared chain is #9345 → #14212 → #12752 → #14829 → #14848. The current #9345 and #14212 heads conflict with main, and the current #14829 head is not included in #14848. These are dependency-state facts, not a conflict in #14848’s own diff.

gh pr checks 14848 --repo boykopovar/AnyPS5:

windows	pending
linux	skipping
register-context	skipping
relinker	skipping
relinker-asan	skipping
write-watch	skipping
conventions	pass
eligibility	pass
intake	pass
labels	pass
library-portability	pass
report	pass

Negative control

Assertion Change to code under test Did the new test fail?
Unregister and termination wait for in-flight callbacks No local mutation or test execution was performed. Could not be exercised here.

Findings

Blocking

The PR and debt entry say firmware 13.60 stores callback/user-data pairs and sends 8-byte login/logout events, but they do not identify the relevant libSceUserService.sprx addresses or explain what each observed site establishes. The reverse-engineering guide requires the module and firmware version, the source locations, what each location shows, and what remains unverified. The tests demonstrate the behavior of this implementation; they do not establish the firmware contract. Please add the source locations and distinguish firmware-observed behavior from host-side lifecycle choices such as waiting for callbacks to drain. If null-callback handling or duplicate-registration behavior was not established from firmware, label it as unverified rather than implying the test confirms it.

Non-blocking

The dependency stack needs reconciliation before landing: #9345 and #14212 currently conflict with main, and #14829’s current head is not included in this branch. The PR lists these dependencies, so I am not asking for an unrelated rebase now.

Sources

Not verified

No local build, CTest, firmware execution, console behavior, or source-module inspection was performed. The Windows hosted job is pending; Linux and specialized jobs were skipped. The author-reported Windows results were not reproduced here.

To get this to a good state

Please add the firmware source locations and identify which claims they support, while marking any host-only drain behavior or unverified error cases as such. When the source and/or code changes, I will rerun the precheck, diff, ABI, stack and test-registration checks and review the new head; the pending hosted jobs should also be refreshed for that SHA.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. A few things before it can go in:

  1. Which thread runs the callbacks. UserServiceSetUserConnected_nid_postfix calls NotifyUserEvent synchronously (core/libs/prx/libSceUserService/Export.cpp:218). Its callers are PadInput::openController, PadInput::closeController and PadInput::applyPort0 (core/libs/prx/libSceVideoOut/src/PadInput.cpp:110, :136, :160), which run from the SDL event loop inside VideoOutDriver::presentLoop (core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp:772-782). So a title's callback runs on the present thread. Any time spent in it delays flips and input. A callback that waits for one of the title's own threads, while that thread waits for a flip, stops presentation for good; the blocking callback in your own test is exactly that shape. Could you deliver the events from a thread of their own (or a guest thread)? If the console's libSceUserService really calls them synchronously from the thread that reports the change, please say where that was read, and note in the debt entry that the emulator calls them on the present thread.

  2. Source of the firmware behaviour. The debt entry (docs/dev/debt/libSceUserService.md:48) says firmware 13.60 stores unique callback and user-data pairs and sends 8-byte login/logout events. It does not say how that was established (disassembly, with offsets?) or that it was not measured on a console. The null handling isn't covered either: sceUserServiceRegisterEventCallback with a null callback returns USER_SERVICE_OK without registering anything (Export.cpp:2912-2913), while sceUserServiceUnregisterEventCallback with a null callback returns USER_SERVICE_ERROR_INVALID_ARGUMENT (Export.cpp:2350-2351). If these follow the module, please cite it in the entry; if they are choices, please say so there. Tests for the two null cases, a duplicate registration, and calls before initialisation would pin them down.

  3. PR description. On main both exports are NotImplemented_nid_no_patch stubs, so this PR implements registration, dispatch and unregistration rather than fixing an existing callback ABI. Also, none of refactor(libSceUserService): retain fail-fast settings call shapes #14829, feat(libSceUserService): take the user name from ANYPS5_USER_NAME #9345 or docs(debt): separate entries with blank lines and drop derived export totals #14212 is in this branch's history, and refactor(libSceUserService): retain fail-fast settings call shapes #14829 still has the stub. Could you update the title and description to say what it adds, and correct the "Depends on" line?

  4. Cyclic unregister deadlock. core/libs/prx/libSceUserService/Export.cpp:128-132 waits for every invocation on another thread, including a callback that is itself unregistering the current callback. With registered callbacks A and B, let notification threads enter A and B, synchronize them, then have A unregister B and B unregister A. Both erase the other registration at Export.cpp:2362-2368 and wait with an in-flight count of one and current-thread depth zero. Neither callback can return. Please prevent this cyclic wait and add a synchronized regression with two notification threads.

  5. Termination while an erased callback runs. core/libs/prx/libSceUserService/Export.cpp:2362-2368 erases a registration before its active callback finishes. While that unregister is waiting, ClearEventCallbacks at Export.cpp:176-183 sees only the remaining registry entries, so sceUserServiceTerminate at Export.cpp:385-392 can return success while the erased callback is still executing. Please retain draining registrations in the termination accounting and test a blocked callback, a concurrent unregister that has removed it, and termination before releasing the callback. A second unregister of that pair also currently returns success during the first drain.

On the tests: core/libs/tests/GuestUserServiceLifecycle.cpp:84-89 uses a sleep without proving the terminator reached the drain. Please synchronize the new regression on the relevant state so an unscheduled terminator cannot satisfy the waiting check. Keep the simple self-unregister and removal-after-snapshot coverage.

@xdCloudy xdCloudy changed the title fix(libSceUserService): make event callbacks reentrant feat(libSceUserService): implement event callback delivery Nov 1, 2026
@xdCloudy

xdCloudy commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the five requested corrections. Delivery now runs on a dedicated host worker, so a blocked title callback does not block the presentation/input caller. Cross-callback delivery is serialized, eliminating the cyclic drain wait. Unregister marks entries inactive but retains them until in-flight calls finish, so termination and a second unregister still account for them. The synchronized regressions include both notification producers and unregister/termination during a blocked callback; 30 consecutive lifecycle runs passed.

The exact registration, unregister, native worker and 8-byte message locations are now cited, including the asymmetric null cases. Host lifecycle choices are identified separately. I corrected the title to describe newly implemented callback delivery and removed the unrelated dependency chain.

AI-assisted: yes.

@github-actions

github-actions Bot commented Nov 1, 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 9345 core/libs/prx/libSceUserService/Export.cpp
core/libs/prx/libSceUserService/UserService.hpp
PR 12860 core/libs/prx/libSceUserService/Export.cpp
docs/dev/debt/libSceUserService.md
PR 12900 core/libs/prx/libSceUserService/Export.cpp
docs/dev/debt/libSceUserService.md
PR 14212 docs/dev/debt/libSceUserService.md

@github-actions github-actions Bot added the build label Nov 1, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 5, 2026
@adriadam10 adriadam10 self-assigned this Nov 6, 2026
Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/libSceUserService-event-callbacks branch from 585a0f0 to 4977956 Compare November 6, 2026 21:05
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 6, 2026
@adriadam10
adriadam10 merged commit f0d7c24 into boykopovar:main Nov 6, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the callback registration/unregister/drain logic, the dedicated delivery worker, the tests (they need the new exports, which were throwing stubs on main) and the debt entries. Rebased onto current main (conflicts in Export.cpp and libSceUserService.md, squashed into one commit) and linked libkernel in the lifecycle test, since Export.cpp now uses sceKernelStat; merged.

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.

4 participants