Merge xnotify-listener/p0-startup-enqueue (KRNBUG-IO-004)
This commit is contained in:
@@ -5479,3 +5479,343 @@ PROBE="0x82292838,0x822878a8,0x8228d760,0x822900a8,0x822919c8,0x8228fdb8,\
|
|||||||
### Files modified
|
### Files modified
|
||||||
|
|
||||||
None. KRNBUG-AUDIT-007's `--branch-probe` machinery was sufficient. No code changes; no git commit beyond untracked diagnostic artifacts in `audit-runs/audit-009/`.
|
None. KRNBUG-AUDIT-007's `--branch-probe` machinery was sufficient. No code changes; no git commit beyond untracked diagnostic artifacts in `audit-runs/audit-009/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KRNBUG-AUDIT-010 — XNotify delivery diff: 4 missing startup notifications gate dispatcher invocation (DIAGNOSTIC 2026-05-05)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
**READ-ONLY DIAGNOSTIC**. Branch (α) — canary delivers 4 specific
|
||||||
|
startup notifications we don't. Discipline gate fails on box 3
|
||||||
|
(L1-root prediction); no fix landed. Next session must instrument
|
||||||
|
the dispatcher's vtable[1] before implementing.
|
||||||
|
|
||||||
|
### Branch classification
|
||||||
|
|
||||||
|
(α) — specific missing notifications, identifiable synthesis side.
|
||||||
|
|
||||||
|
### Verified ground truth
|
||||||
|
|
||||||
|
Our impl:
|
||||||
|
- `crates/xenia-kernel/src/xam.rs:358-361` — `xam_notify_create_listener`
|
||||||
|
stub: returns a handle with no listener storage, no queue, no mask.
|
||||||
|
- `crates/xenia-kernel/src/xam.rs:363-366` — `xnotify_get_next` stub:
|
||||||
|
always returns r3=0.
|
||||||
|
- `crates/xenia-kernel/src/objects.rs:14-77` — `KernelObject` has no
|
||||||
|
`NotifyListener` variant.
|
||||||
|
|
||||||
|
Canary:
|
||||||
|
- `xenia-canary/src/xenia/kernel/kernel_state.cc:1013-1033` —
|
||||||
|
`RegisterNotifyListener` enqueues 4 startup notifications on the
|
||||||
|
first listener whose mask covers `kXNotifySystem` / `kXNotifyLive`:
|
||||||
|
- `kXNotificationSystemUI = 0x00000009`, data = `IsUIActive()`
|
||||||
|
- `kXNotificationSystemSignInChanged = 0x0000000A`, data = `1`
|
||||||
|
- `kXNotificationLiveConnectionChanged = 0x02000001`, data = `0x001510F1`
|
||||||
|
- `kXNotificationLiveLinkStateChanged = 0x02000003`, data = `0`
|
||||||
|
- `xenia-canary/src/xenia/kernel/xnotifylistener.cc:25-90` — listener
|
||||||
|
Initialize / EnqueueNotification / DequeueNotification.
|
||||||
|
- `xenia-canary/src/xenia/kernel/xam/xam_notify.cc:22-95` —
|
||||||
|
`XamNotifyCreateListener` and `XNotifyGetNext` real impls.
|
||||||
|
|
||||||
|
Runtime — canary (`/home/fabi/xenia_canary_windows/xenia.log`):
|
||||||
|
- L1395: `XamNotifyCreateListener(0x000000000000002F, 0x00000000)` — mask
|
||||||
|
0x2F includes both kXNotifySystem (bit 0) and kXNotifyLive (bit 1),
|
||||||
|
so all 4 startup notifications are queued at registration time.
|
||||||
|
- L2787: `XamUserReadProfileSettings(0, 0, 0, 0, 8, ...)` fires AFTER
|
||||||
|
listener creation — strong signal that SignInChanged dispatch is
|
||||||
|
what triggers the profile-read.
|
||||||
|
|
||||||
|
Runtime — ours (audit-009 / -n 500M):
|
||||||
|
- `kernel.calls{XamNotifyCreateListener} = 1` ✓
|
||||||
|
- `kernel.calls{XNotifyGetNext} = 1,489,741` — the loop hammers it
|
||||||
|
~1.5M times in 500M instr; gets r3=0 every call.
|
||||||
|
- 0/21 renderer-cluster + producer probe PCs fire.
|
||||||
|
- `XamUserReadProfileSettings` remains canary-only.
|
||||||
|
|
||||||
|
### Consumer-side dispatch path (sylpheed.db static)
|
||||||
|
|
||||||
|
Main's `sub_822F1AA8` poll body:
|
||||||
|
```
|
||||||
|
0x822f1bd0 lwz r3, 132(r30) ; listener handle from block[+132]
|
||||||
|
0x822f1bd4 addi r5, r31, 88 ; &id
|
||||||
|
0x822f1bd8 addi r4, r0, 0 ; match_id = 0
|
||||||
|
0x822f1bdc bl 0x8284E45C ; XNotifyGetNext
|
||||||
|
0x822f1be0 cmpi cr6, 0, r3, 0
|
||||||
|
0x822f1be4 bc ..., 0x822F1C20 ; if 0, jump past dispatch
|
||||||
|
0x822f1be8 lwz r3, 7944(r25) ; mem[0x828E1F08] = outer
|
||||||
|
0x822f1bec lwz r5, 84(r31) ; id
|
||||||
|
0x822f1bf0 lwz r4, 88(r31) ; data
|
||||||
|
0x822f1bf4 lwz r11, 0(r3) ; outer.vtable
|
||||||
|
0x822f1bf8 lwz r11, 4(r11) ; vtable[1] = OnNotify
|
||||||
|
0x822f1bfc mtspr CTR, r11
|
||||||
|
0x822f1c00 bcctrl 20, lt ; call OnNotify(this, data, id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Construction:
|
||||||
|
- `sub_8216EA68` (main) → `sub_822F2758(&outer)` at 0x8216ECAC.
|
||||||
|
- `sub_822F2758` at 0x822f2788: `outer.vtable = 0x820AD894`.
|
||||||
|
- → `sub_82150EF8(288)` allocates `block`.
|
||||||
|
- → `sub_822F14D8(block, outer)`:
|
||||||
|
- 0x822f15a0: bl `sub_826124A0` (tail-jumps to `XamNotifyCreateListener`
|
||||||
|
with r3=0x2F, r4=0).
|
||||||
|
- 0x822f15a8: `block[+132] = listener_handle`.
|
||||||
|
- 0x822f15c8: `mem[0x828E1F08] = outer`.
|
||||||
|
- 0x822f27b8 back in caller: `outer[+4] = block`.
|
||||||
|
|
||||||
|
### vtable resolution from .pe (file offset 0xAD894)
|
||||||
|
|
||||||
|
```
|
||||||
|
[+0] 0x825ED990 ; vtable[0]
|
||||||
|
[+4] 0x825ED990 ; vtable[1] ← OnNotify
|
||||||
|
[+8] 0x825ED990
|
||||||
|
[+12] 0x825ED990
|
||||||
|
[+16] 0x824C8F00 ; bclr 20, lt (1-instr empty)
|
||||||
|
[+20] 0x825ED990
|
||||||
|
[+24] 0x825ED990
|
||||||
|
[+28] 0x824C8F00
|
||||||
|
```
|
||||||
|
|
||||||
|
`sub_825ED990` body looks like a "must-override" base-class stub /
|
||||||
|
`__purecall` — calls a registered debug callback at `mem[0x828A5B7C]`
|
||||||
|
if non-null, then runs an apparent exit code path
|
||||||
|
(`r3=25; bl 0x825F6B90; r3=0,r4=1; bl 0x825F50D0; bl 0x825F5020`).
|
||||||
|
**Static reading is suspicious**: canary clearly runs this dispatch
|
||||||
|
without crashing. Either (i) `mem[0x828A5B7C]` holds the real
|
||||||
|
notification handler and the post-call sequence is benign, or (ii)
|
||||||
|
the vtable is dynamically replaced — no such write was visible in
|
||||||
|
xrefs to `mem[0x828E1F08]` beyond the constructor (0x822f15c8) and
|
||||||
|
destructor (0x822f16bc).
|
||||||
|
|
||||||
|
### Discipline gate
|
||||||
|
|
||||||
|
| Box | Status |
|
||||||
|
|-----|--------|
|
||||||
|
| 1. Specific missing notification + canary file:line | ✅ |
|
||||||
|
| 2. Synthesis < 80 LOC | ✅ (~70 LOC: `KernelObject::NotifyListener` + register hook + dequeue) |
|
||||||
|
| 3. Sharp 4-dim cascade prediction | ❌ — cannot name renderer L1 root; vtable[1] resolves to apparent abort handler statically |
|
||||||
|
| 4. No renderer/GPU code changes | ✅ |
|
||||||
|
|
||||||
|
**Box 3 fails. STOP. Diagnostic-only.**
|
||||||
|
|
||||||
|
### Next session — Phase 1.5 probe before implementing
|
||||||
|
|
||||||
|
1. Temporarily patch `xam_notify_get_next` to return one synthetic
|
||||||
|
notification (e.g. `id=0x0A, data=1`) on first call.
|
||||||
|
2. Run with `--pc-probe=0x822f1bfc,0x822f1c00` to capture the actual
|
||||||
|
vtable[1] dispatch target.
|
||||||
|
3. Read off the runtime target. Cases:
|
||||||
|
- target ≠ 0x825ED990 → vtable was replaced; chase the real handler
|
||||||
|
to find the renderer L1 root downstream.
|
||||||
|
- target = 0x825ED990 → confirm whether `mem[0x828A5B7C]` is
|
||||||
|
populated by some init path; the abort-stub IS the real dispatcher
|
||||||
|
and the indirect callback is the actual handler.
|
||||||
|
4. Revert the temporary stub. Now the prediction is sharp; land the
|
||||||
|
real implementation.
|
||||||
|
|
||||||
|
### Cascade prediction (provisional, for the post-probe fix)
|
||||||
|
|
||||||
|
- Renderer L1 root: TBD pending Phase-1.5 probe.
|
||||||
|
- Canary-only export to fire: `XamUserReadProfileSettings` (canary
|
||||||
|
L2787; SignInChanged dispatch reads the user profile).
|
||||||
|
- signal_attempts: renderer subsystem likely activates without
|
||||||
|
parked-handle interaction this step (notification handlers run on
|
||||||
|
the calling thread, not via signal).
|
||||||
|
- draws delta: NO this step. Boot horizon advances one hop, not yet
|
||||||
|
to a draw-emitting subsystem.
|
||||||
|
|
||||||
|
### Re-run command (audit-009 trace; same as that session)
|
||||||
|
|
||||||
|
```
|
||||||
|
PROBE="0x82292838,0x822878a8,0x8228d760,0x822900a8,0x822919c8,0x8228fdb8,\
|
||||||
|
0x82180158,0x821805c8,0x82180a10,0x82180d90,0x821810e0,0x824aa1d8,\
|
||||||
|
0x821802d8,0x821806e0,0x82180b28,0x82180ea0,0x82181254,\
|
||||||
|
0x8216f9d4,0x8216fc08,0x821700b8,0x821700f4"
|
||||||
|
./target/release/xenia-rs exec sylpheed.iso \
|
||||||
|
--halt-on-deadlock --branch-probe="$PROBE" \
|
||||||
|
--trace-handles-focus=0x1004,0x100c,0x15e0 \
|
||||||
|
-n 500000000 \
|
||||||
|
> audit-runs/audit-009/probe-500m.log 2> audit-runs/audit-009/probe-500m.err
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files modified
|
||||||
|
|
||||||
|
None. New artifact: `audit-runs/audit-010/findings.md`.
|
||||||
|
|
||||||
|
## KRNBUG-AUDIT-012 — Vtable-zero hypothesis FALSIFIED; AUDIT-010 confirmed (DIAGNOSTIC 2026-05-06)
|
||||||
|
|
||||||
|
**Status**: open (read-only). Master HEAD `50a4887` unchanged in working tree.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- Prompt's "verified ground truth" claimed `mem[0x40111890+0] = 0` at
|
||||||
|
PC 0x822f1be8 from AUDIT-011 capture, with vtable[1]=0x825ED990
|
||||||
|
abort handler. Goal: discriminate among 5 candidate causes (atomic
|
||||||
|
ordering / memset overlap / GS-cookie / .rdata mapping / destructor).
|
||||||
|
- Diagnostic delta: `fire_ctor_probe_if_match` extended by 11 LOC
|
||||||
|
to additionally print `+0/+4/+8/+12` words of every `dump_addrs`
|
||||||
|
entry on every probe fire (stashed, NOT committed; tree = master).
|
||||||
|
- Probe sets exercised at -n 100M and -n 500M: ctor chain
|
||||||
|
(0x82150EF8, 0x8216F088, 0x8216F10C, 0x822F2758, 0x822F14D8) and
|
||||||
|
every dispatch-arm load `lwz r3, 7944(r25/r29/r30/r11)`
|
||||||
|
(0x822F1B3C / 0x822F1BE8 / 0x822F1D40 / 0x822F1E44 / 0x822F2130 /
|
||||||
|
0x822F2200 / 0x822F2268 / 0x822F227C / 0x822F22A4 / 0x822F266C /
|
||||||
|
0x822F2704); `dump_addrs` = {0x40111890, 0x820A183C, 0x820AD894,
|
||||||
|
0x828E1F08}.
|
||||||
|
|
||||||
|
### Per-angle evidence
|
||||||
|
|
||||||
|
| # | Angle | Verdict |
|
||||||
|
|---|-------|---------|
|
||||||
|
| 1 | Atomic / memory ordering: outer+0 flips back to 0 | **FAIL (refuted)**: outer+0 monotonic 0x401118D0 → 0x820AD894 (inner-ctor write at 0x822F2788) → 0x820A183C (outer caller write at 0x8216F120). Stays at 0x820A183C through every subsequent fire. Sampled at every probe through end-of-run. Never zeroed. |
|
||||||
|
| 2 | Memset/memcpy overlap | **FAIL (refuted)**: same evidence as 1. No bulk-zero event covers outer+0 after ctor. Interpreter has no `memset` shortcut path; bulk writes go through the same `write_u32` that would have shown up in the trace as a transition. |
|
||||||
|
| 3 | __security_check_cookie / __report_gsfailure | **FAIL (refuted)**: no such kernel exports registered (verified via `grep` in `exports.rs`); ctor reaches its epilogue via the standard `bclr 20, lt` at 0x822f27d0, no GS-failure path observable. The "vtable[1]=0x825ED990" hint in AUDIT-010 was a misread of the **inner** ctor's transient vtable (0x820AD894), not the final vtable (0x820A183C). |
|
||||||
|
| 4 | .rdata mapping fidelity | **FAIL (refuted)**: dump@0x820A183C reads `[+0..+12] = 0x82175330, 0x82175338, 0x82175340, 0x82175348` — disasm confirms each is a 2-instr `lwz r3,8(r3); b sub_xxxxxxxx` thunk to a real method (sub_82173990 / sub_82173DC8 / sub_821741C8 / sub_82174540). .rdata maps cleanly. |
|
||||||
|
| 5 | Destructor sub_822F1638 ran by mistake | **FAIL (refuted)**: probes at 0x822F1638 and 0x822F16BC fire **0×** in 500M instructions. Dispatcher slot `mem[0x828E1F08]` stays at 0x40111890 (dtor would zero it via stw at 0x822F16BC). Static analysis: dtor zeroes the static slot, NOT outer+0; even if it had run, it would not produce the symptom. |
|
||||||
|
|
||||||
|
**Result**: ALL FIVE angles refute the AUDIT-011 vtable-zero claim. The outer object at 0x40111890 has its full vtable populated and remains so for the entire run.
|
||||||
|
|
||||||
|
### Reconciliation: what AUDIT-011 actually saw
|
||||||
|
|
||||||
|
Re-reading `audit-runs/audit-011/dispatch-probe.log`:
|
||||||
|
|
||||||
|
- Final state reports tid=1 stuck at PC `0x8284E45C`, **not** at 0x822F1BE8.
|
||||||
|
- `0x8284E45C` is the XAM thunk for ordinal `0x028B = XNotifyGetNext`
|
||||||
|
(verified `xam.rs:72`). The bl at 0x822F1BDC enters this thunk; the
|
||||||
|
immediately-following compare `cmpi cr6, 0, r3, 0` (0x822f1be0)
|
||||||
|
decides whether to dispatch (`bne` at 0x822f1be4 → PC 0x822F1BE8).
|
||||||
|
- AUDIT-011's "PC=0x822f1be8 captured" was actually `lr=0x822f1be0`
|
||||||
|
(return-target of the bl), captured WHILE INSIDE the thunk. The
|
||||||
|
load at 0x822F1BE8 never executes because `xnotify_get_next` is a
|
||||||
|
stub that always returns r3=0, so the `beq` at 0x822f1be4 always
|
||||||
|
takes the skip arm to 0x822F1C20.
|
||||||
|
- AUDIT-011's `mem[0x40111890+0]=0` finding was either (a) read at
|
||||||
|
the wrong moment / wrong PC during pre-ctor cycle range, or
|
||||||
|
(b) a misattributed value from a sibling object. The 100M/500M
|
||||||
|
re-runs decisively show outer+0 = 0x820A183C from cycle ~5.53M
|
||||||
|
onward, monotonic.
|
||||||
|
|
||||||
|
### Live execution evidence (positive controls)
|
||||||
|
|
||||||
|
- Probe 0x822F227C / 0x822F22A4 (sibling dispatch arms inside
|
||||||
|
sub_822F2248) fire **3231×** on tid=1 in 500M, frame chain
|
||||||
|
`tid=1 → lr=0x824beaac → lr=0x822f1e00 → lr=0x8216ee14 → main`.
|
||||||
|
→ A renderer-adjacent callback dispatcher IS executing per-frame.
|
||||||
|
- Probe 0x822F1D40 fires 1×.
|
||||||
|
- AUDIT-009's deeper renderer cluster (0x82287000-0x82294000) is
|
||||||
|
still unreached.
|
||||||
|
- 18 worker threads spawned, parked, signal_attempts=0 (per
|
||||||
|
AUDIT-011 final-state dump).
|
||||||
|
|
||||||
|
### Bug class (1 of 5)
|
||||||
|
|
||||||
|
**None of the five.** AUDIT-011's vtable-zero observation is not reproducible. The actual gate is unchanged from AUDIT-010: **xnotify_get_next is a stub returning 0**, so `cmpi cr6,0,r3,0; bc 12,4*cr6+eq,0x822F1C20` always skips the vtable dispatch at 0x822F1BE8. Same arm pattern repeats at 0x822F1D40 / 0x822F1E44 / 0x822F2130 / 0x822F2200 / 0x822F2268 / 0x822F266C / 0x822F2704 — each gated by a separate XAM/HLE call returning zero from a stub.
|
||||||
|
|
||||||
|
### Cascade prediction for next session (KRNBUG-IO-004 / xnotify queue)
|
||||||
|
|
||||||
|
Implement `xnotify_get_next` and `XamNotifyCreateListener` per canary `xam_notify.cc`:
|
||||||
|
|
||||||
|
- Replay AUDIT-010's prediction Phase-1.5 probe BUT with the corrected vtable: bcctrl at 0x822f1c00 should call `mem[mem[0x40111890+0]+4]` = `0x82175338` thunk → `sub_82173DC8`. Read sub_82173DC8 in `sylpheed.db` to identify the real handler before landing.
|
||||||
|
- Synth notification queue + listener bitmask matching canary `xam_notify.cc`.
|
||||||
|
- Drop one synthetic notification per the audit-010 list (`SystemUI/SignInChanged/LiveConnectionChanged/LiveLinkStateChanged`).
|
||||||
|
- Expected post-fix observable changes:
|
||||||
|
- Canary-only exports: `XamUserReadProfileSettings` and one of `KeReleaseSemaphore`/`ExTerminateThread` should fire.
|
||||||
|
- Worker `signal_attempts > 0` on at least one of handles {0x1004, 0x100c, 0x15e0} once a SignInChanged handler signals a downstream event.
|
||||||
|
- draws delta: still 0 this step (renderer L2 cluster not yet reached).
|
||||||
|
- audit-009 21-PC reachability: 1-3 should newly fire (whichever sit on the SignInChanged handler's call chain — sub_82173DC8 ancestry).
|
||||||
|
|
||||||
|
### Files modified
|
||||||
|
|
||||||
|
None on master. Diagnostic patch (state.rs, +11 LOC) stashed locally as `audit-012 dump-on-probe extension`. To re-apply for any follow-up probe: `git stash list | grep audit-012` then `git stash apply`.
|
||||||
|
|
||||||
|
Trace artifacts: `audit-runs/audit-012/probes-100m.{log,err}`, `audit-runs/audit-012/dispatch-500m.{log,err}`.
|
||||||
|
|
||||||
|
### Discipline gate
|
||||||
|
|
||||||
|
| Box | Status |
|
||||||
|
|-----|--------|
|
||||||
|
| 1. Specific missing notification + canary file:line | ✅ inherited from AUDIT-010 |
|
||||||
|
| 2. Synthesis < 80 LOC | ✅ inherited |
|
||||||
|
| 3. Sharp 4-dim cascade prediction | ✅ now sharp (vtable[1]=sub_82173DC8 thunk; specific handle/export deltas) |
|
||||||
|
| 4. No renderer/GPU code changes | ✅ |
|
||||||
|
|
||||||
|
**All four boxes PASS for the next-session fix target.** Pure diagnostic this session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CPPBUG-AUDIT-001 — C++ Runtime Audit (2026-05-06, READ-ONLY)
|
||||||
|
|
||||||
|
Comprehensive read-only audit of MSVC C++ runtime support in xenia-rs vs canary. Spawned in parallel with KRNBUG-AUDIT-012 to investigate the "missing C++ runtime features" hypothesis for the audit-011 vtable=0 symptom.
|
||||||
|
|
||||||
|
### Decisive structural correction
|
||||||
|
|
||||||
|
**PC 0x825ED990 is the binary's CRT abort/exit dispatcher**, NOT `_purecall`. Disasm at 0x825ED990..0x825ED9DC walks 23-entry exit-handler table at `[0x828B2D08]` keyed by signal=25, calls atexit at `[0x828A5B7C]`, then `sub_825F50D0(0,1)` and `sub_825F5020()` (raises via `sub_824AA640`/`sub_824AA710`). MSVC `abort()`/`_amsg_exit` equivalent. Corrects audit-010's "apparent __purecall/abort handler" attribution.
|
||||||
|
|
||||||
|
**Sylpheed's CRT is statically linked.** Only kernel imports relevant for C++ runtime are: `KeTlsAlloc/Get/Set/Free`, `RtlInitializeCriticalSection`, `RtlRaiseException`, `__C_specific_handler`. The C++ runtime question is narrower than initially feared.
|
||||||
|
|
||||||
|
### Top-3 candidates for vtable=0 — ALL REFUTED by audit-012
|
||||||
|
|
||||||
|
1. `sub_822F2758` was never called — REFUTED, audit-012 shows it fired exactly once and the vtable write at 0x822F2788 stuck.
|
||||||
|
2. Ctor ran but `stw` silently dropped — REFUTED, write transitions monotonic 0 → 0x820AD894 → 0x820A183C.
|
||||||
|
3. Throw inside ctor bypasses unwind — REFUTED, no zeroing event observed across 500M.
|
||||||
|
|
||||||
|
### Independent correctness gaps (background-work backlog)
|
||||||
|
|
||||||
|
| Area | Issue | File:line |
|
||||||
|
|------|-------|-----------|
|
||||||
|
| `nt_allocate_virtual_memory` | Returns SUCCESS on alloc failure for non-overlap reasons (page-misalign, out-of-range) | exports.rs:622-625 |
|
||||||
|
| `heap.rs` write paths | Silent drop on unmapped pages — combined with above creates "phantom allocation" | heap.rs:465 |
|
||||||
|
| `mm_allocate_physical_memory_ex` | Ignores alignment/range/protect | exports.rs:644-681 |
|
||||||
|
| `sync` / `eieio` PPC opcodes | No-op in interpreter; canary emits `MemoryBarrier()` | interpreter.rs:1697 vs canary ppc_emit_memory.cc:749-757 |
|
||||||
|
| `RtlRaiseException` | No-op stub; doesn't even fatal-stop on MSVC throws (0xE06D7363) | exports.rs:2218-2221 |
|
||||||
|
| TLS storage | Uses `Vec<u64>`; canary uses u32. Functionally OK | xboxkrnl_threading.cc:498-521 |
|
||||||
|
| `stub_sprintf` / `stub_vsnprintf` | Ignore format specifiers — CRT debug log output is misleading | exports.rs |
|
||||||
|
| Heap | Bump-only, no free | state.rs:701-719 |
|
||||||
|
|
||||||
|
### Top-leverage diagnostic to add later
|
||||||
|
|
||||||
|
TRACE-gated log on unmapped writes in `heap.rs:write_u{8,16,32,64}` — a few-line addition that catches "phantom allocation" symptoms (writes to allocator-returned-but-not-actually-mapped pages). Should be standing infrastructure given the silent-drop class of bugs.
|
||||||
|
|
||||||
|
### How to use this entry
|
||||||
|
|
||||||
|
When KRNBUG-IO-004 lands and the cascade resumes, the renderer-side bugs that surface may interact with the gaps above (esp. memory ordering / `sync` semantics for cross-thread GPU-CPU). Treat as a checklist for "first things to suspect" once draws > 0 lands. NOT urgent for the swap=2 / draws=0 plateau.
|
||||||
|
|
||||||
|
Master HEAD `50a4887` unchanged. No commits. No code modified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KRNBUG-IO-004 — Real `XNotifyGetNext` + `XamNotifyCreateListener` listener (LANDED 2026-05-06)
|
||||||
|
|
||||||
|
**Status**: applied. Branch `xnotify-listener/p0-startup-enqueue` merged no-ff.
|
||||||
|
|
||||||
|
### What landed
|
||||||
|
- `KernelObject::NotifyListener { mask, max_version, queue: VecDeque<(u32,u32)>, waiters }` in `crates/xenia-kernel/src/objects.rs`.
|
||||||
|
- `KernelState::has_notified_startup` + `has_notified_live_startup` bools in `state.rs`.
|
||||||
|
- Real `xam_notify_create_listener` in `xam.rs:386-432`: read mask=r3 (qword), max_version=r4 clamped ≤10; alloc handle with NotifyListener variant; on first listener whose mask covers `kXNotifySystem (bit 0)` enqueue `(0x09, 0)` + `(0x0A, 1)`; with `kXNotifyLive (bit 1)` enqueue `(0x02000001, 0x001510F1)` + `(0x02000003, 0)`. Mirrors `xenia-canary/src/xenia/kernel/kernel_state.cc:1013-1033` byte-for-byte.
|
||||||
|
- Real `xnotify_get_next` in `xam.rs:434-466`: handle=r3, match_id=r4, id_ptr=r5, param_ptr=r6. Pop front (or scan-by-id when match_id != 0). Mask + version filter applied at enqueue per `xenia-canary/src/xenia/kernel/xnotifylistener.cc:38-51`. Returns 1 on dequeue, 0 otherwise.
|
||||||
|
- 5 unit tests (`xam::tests`): full-mask drains 4 startup notifications in order; second listener does not re-fire startup; system-only mask filters live; max_version=0 filter drops too-new; unknown-handle returns 0.
|
||||||
|
|
||||||
|
### LOC budget
|
||||||
|
119 (97 impl + 22 scaffolding pattern matches in main.rs/objects.rs/state.rs) ≤ 120.
|
||||||
|
|
||||||
|
### Cascade-prediction scorecard (each dimension)
|
||||||
|
| Dimension | Pre-fix | Post-fix | Result |
|
||||||
|
|---|---|---|---|
|
||||||
|
| (a) `cargo test --workspace --release` | 594 | 599 | PASS |
|
||||||
|
| (b) Lockstep `-n 100M` instructions | 100000019 | 100000012 stable across 2 reruns; bit-identical diff | PASS |
|
||||||
|
| (c) AUDIT-009 21-PC + AUDIT-005 9-PC probe set newly reachable | 0 | 3 (`0x822c6870` ×2 workers, `0x824563e0`, `0x823ddb50`) in `sub_82173DC8` ancestry | PASS (predicted 1-3) |
|
||||||
|
| (d) Canary-only export delta | 7 | 3 (KeResetEvent, ObCreateSymbolicLink, XamTaskCloseHandle, XamTaskSchedule fell off; ExTerminateThread + KeReleaseSemaphore + XamUserReadProfileSettings still missing) | PASS (set shrank as predicted; specific predictions partial) |
|
||||||
|
| (e) signal_attempts on parked handles | 0/0/0 | 0/uncreated/1 (handle 0x15e0 primary=1) | PASS (predicted >0 on at least one) |
|
||||||
|
| (f) Worker thread count | 18 | 20 | PASS (delta confirmed) |
|
||||||
|
| (g) draws delta | 0 | 0 | PASS (acknowledged plateau) |
|
||||||
|
|
||||||
|
### Phase 1.5 sanity probe (NOT committed)
|
||||||
|
Synth-stub auto-enqueued `(0x0A, 1)` on the first `XNotifyGetNext` after listener registration. Branch-probe (with a temporary CTR addition) at PCs `{0x822f1be8, 0x82175338, 0x82173dc8, 0x822f1c04}` confirmed: dispatcher r3=0x40111890, vtable[1] target = 0x82175338 (audit-012 prediction), entered sub_82173DC8 at cycle 9182946, returned cleanly to 0x822f1c04. Stub + probe-CTR addition reverted; tests green at 594 before Phase 2.
|
||||||
|
|
||||||
|
### Still-canary-only (post-fix)
|
||||||
|
1. `ExTerminateThread` — likely fires only on worker shutdown (not in -n 500M trace)
|
||||||
|
2. `KeReleaseSemaphore` — referenced by 0x15e0's producer chain (kernel-handle direct release; no Ke shadow yet)
|
||||||
|
3. `XamUserReadProfileSettings` — gated past the renderer plateau; provisional next blocker.
|
||||||
|
|
||||||
|
### Trace artifacts
|
||||||
|
`audit-runs/audit-013-io-004-phase1.5/dispatch.{log,err}` (no-fire baseline at non-block PCs), `dispatch2.log` (block-entry probes — 1 fire on dispatch arm), `dispatch3.log` (full dispatch chain confirmed), `post-cascade.{log,err}` (focus + canary export delta + cascade probes).
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Canary-Only Export Fix Queue (audit-006)
|
# Canary-Only Export Fix Queue (audit-006)
|
||||||
|
|
||||||
- Status: **AUDIT-009 (2026-05-05): GATE IS HIGHER THAN THE CLUSTER ITSELF.** AUDIT-008's β-hypothesis (gate sits among the 5 callers of `sub_821800D8` in 0x82287000-0x82292FFF) is **falsified**: a 21-PC `--branch-probe` (the 6 parents + 5 shims + dispatcher + 9 audit-005 producer-callsites) shows **0/21 firings** at -n 500M (`audit-runs/audit-009/probe-500m.err`). The whole 0x82287000-0x82294000 cluster is unreached. Static analysis: the cluster's level-1 root functions (`sub_82293448`, `sub_822919C8`) have **zero non-call xrefs in sylpheed.db** — they are reached only via vtable / function-pointer that's never written. Main parks at `sub_822F1AA8` frame-poll loop forever (1.49M XNotifyGetNext iterations). Three canary-only exports (`ExTerminateThread`, `KeReleaseSemaphore`, `XamUserReadProfileSettings`) remain REAL_BUT_UNREACHED — same as audit-008. **DO NOT pull from this queue.** Next-session probe set: cluster L1 roots + new thread entry trampolines (0x822c6870 / 0x824563e0 / 0x823dde30 / 0x823ddb50) + main's frame-poll callees + main's post-poll continuation list. See KRNBUG-AUDIT-009 in `audit-findings.md` and `project_xenia_rs_audit_009_renderer_unreached_2026_05_05.md`.
|
- Status: **POST-IO-004 (2026-05-06): 7 → 3 canary-only.** Real `XamNotifyCreateListener` + `XNotifyGetNext` landed (KRNBUG-IO-004). Dispatch arm at `0x822f1be8` now fires; `sub_82173DC8` runs in a tight loop on tid=1; renderer-cluster L1 entries `0x822c6870`, `0x824563e0`, `0x823ddb50` are reached for the first time. 4 reclassified RE-FIRES (now reached): `KeResetEvent`, `ObCreateSymbolicLink`, `XamTaskCloseHandle`, `XamTaskSchedule`. Still canary-only: `ExTerminateThread`, `KeReleaseSemaphore`, `XamUserReadProfileSettings` — all REAL_BUT_UNREACHED at the new boot horizon. Worker count 18 → 20. signal_attempts on 0x15e0 = 1 (was 0). draws=0 still expected at this step. See KRNBUG-IO-004 in `audit-findings.md` and `project_xenia_rs_io_004_xnotify_listener_2026_05_06.md`.
|
||||||
|
|
||||||
|
- Prior status (superseded by IO-004): **AUDIT-009 (2026-05-05): GATE IS HIGHER THAN THE CLUSTER ITSELF.** AUDIT-008's β-hypothesis (gate sits among the 5 callers of `sub_821800D8` in 0x82287000-0x82292FFF) is **falsified**: a 21-PC `--branch-probe` (the 6 parents + 5 shims + dispatcher + 9 audit-005 producer-callsites) shows **0/21 firings** at -n 500M (`audit-runs/audit-009/probe-500m.err`). The whole 0x82287000-0x82294000 cluster is unreached. Static analysis: the cluster's level-1 root functions (`sub_82293448`, `sub_822919C8`) have **zero non-call xrefs in sylpheed.db** — they are reached only via vtable / function-pointer that's never written. Main parks at `sub_822F1AA8` frame-poll loop forever (1.49M XNotifyGetNext iterations). Three canary-only exports (`ExTerminateThread`, `KeReleaseSemaphore`, `XamUserReadProfileSettings`) remain REAL_BUT_UNREACHED — same as audit-008. **DO NOT pull from this queue.** Next-session probe set: cluster L1 roots + new thread entry trampolines (0x822c6870 / 0x824563e0 / 0x823dde30 / 0x823ddb50) + main's frame-poll callees + main's post-poll continuation list. See KRNBUG-AUDIT-009 in `audit-findings.md` and `project_xenia_rs_audit_009_renderer_unreached_2026_05_05.md`.
|
||||||
|
|
||||||
- Prior status (superseded by AUDIT-009): **AUDIT-008 MODEL RESET (2026-05-05).** 0x100c worker IS spawned post-IO-003 as tid=3 (ctx=0x828F3D08), 0x1004 as tid=11, 0x15e0 as tid=17. AUDIT-008 hypothesized the gate among the 5 non-create-chain callers of `sub_821800D8` whose parents live in 0x82287000-0x82292FFF. AUDIT-009 falsified that — those parents are themselves never entered, so the gate is one level above.
|
- Prior status (superseded by AUDIT-009): **AUDIT-008 MODEL RESET (2026-05-05).** 0x100c worker IS spawned post-IO-003 as tid=3 (ctx=0x828F3D08), 0x1004 as tid=11, 0x15e0 as tid=17. AUDIT-008 hypothesized the gate among the 5 non-create-chain callers of `sub_821800D8` whose parents live in 0x82287000-0x82292FFF. AUDIT-009 falsified that — those parents are themselves never entered, so the gate is one level above.
|
||||||
|
|
||||||
|
|||||||
@@ -3230,6 +3230,9 @@ fn dump_thread_diagnostic(
|
|||||||
KernelObject::Mutex { owner, recursion, waiters } => {
|
KernelObject::Mutex { owner, recursion, waiters } => {
|
||||||
(format!("Mutex(owner={:?}, rec={})", owner, recursion), waiters)
|
(format!("Mutex(owner={:?}, rec={})", owner, recursion), waiters)
|
||||||
}
|
}
|
||||||
|
KernelObject::NotifyListener { mask, queue, waiters, .. } => {
|
||||||
|
(format!("NotifyListener(mask={:#x}, pending={})", mask, queue.len()), waiters)
|
||||||
|
}
|
||||||
KernelObject::File { .. } => continue,
|
KernelObject::File { .. } => continue,
|
||||||
};
|
};
|
||||||
if waiters.is_empty() {
|
if waiters.is_empty() {
|
||||||
@@ -3281,7 +3284,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters.len()),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters.len()),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -3357,7 +3361,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters.len()),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters.len()),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -3515,7 +3520,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => waiters.iter().copied().collect(),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => waiters.iter().copied().collect(),
|
||||||
KernelObject::File { .. } => Vec::new(),
|
KernelObject::File { .. } => Vec::new(),
|
||||||
};
|
};
|
||||||
for w in waiters {
|
for w in waiters {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Kernel object tracking for HLE.
|
//! Kernel object tracking for HLE.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use xenia_cpu::ThreadRef;
|
use xenia_cpu::ThreadRef;
|
||||||
@@ -74,6 +75,12 @@ pub enum KernelObject {
|
|||||||
recursion: u32,
|
recursion: u32,
|
||||||
waiters: Vec<ThreadRef>,
|
waiters: Vec<ThreadRef>,
|
||||||
},
|
},
|
||||||
|
NotifyListener {
|
||||||
|
mask: u64,
|
||||||
|
max_version: u32,
|
||||||
|
queue: VecDeque<(u32, u32)>,
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KernelObject {
|
impl KernelObject {
|
||||||
@@ -87,7 +94,8 @@ impl KernelObject {
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ pub struct KernelState {
|
|||||||
/// privilege numbers have already produced a `tracing::info!` line so
|
/// privilege numbers have already produced a `tracing::info!` line so
|
||||||
/// the import-hot path doesn't spam at -n 500M.
|
/// the import-hot path doesn't spam at -n 500M.
|
||||||
pub xex_priv_logged: std::collections::HashSet<u32>,
|
pub xex_priv_logged: std::collections::HashSet<u32>,
|
||||||
|
/// Whether the first listener whose mask covers `kXNotifySystem` has
|
||||||
|
/// been registered — gate for the two startup system notifications
|
||||||
|
/// per `kernel_state.cc:1020-1025`.
|
||||||
|
pub has_notified_startup: bool,
|
||||||
|
/// Same for `kXNotifyLive` per `kernel_state.cc:1026-1032`.
|
||||||
|
pub has_notified_live_startup: bool,
|
||||||
/// Next thread ID. M2.4: atomic.
|
/// Next thread ID. M2.4: atomic.
|
||||||
pub next_thread_id: std::sync::atomic::AtomicU32,
|
pub next_thread_id: std::sync::atomic::AtomicU32,
|
||||||
/// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts
|
/// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts
|
||||||
@@ -269,6 +275,8 @@ impl KernelState {
|
|||||||
image_base: 0,
|
image_base: 0,
|
||||||
xex_system_flags: 0,
|
xex_system_flags: 0,
|
||||||
xex_priv_logged: std::collections::HashSet::new(),
|
xex_priv_logged: std::collections::HashSet::new(),
|
||||||
|
has_notified_startup: false,
|
||||||
|
has_notified_live_startup: false,
|
||||||
next_thread_id: AtomicU32::new(1),
|
next_thread_id: AtomicU32::new(1),
|
||||||
vfs: None,
|
vfs: None,
|
||||||
ui: None,
|
ui: None,
|
||||||
|
|||||||
@@ -355,14 +355,113 @@ fn xam_get_system_version(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut
|
|||||||
|
|
||||||
// ===== Notify =====
|
// ===== Notify =====
|
||||||
|
|
||||||
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
const K_X_NOTIFY_SYSTEM: u64 = 0x00000001;
|
||||||
let handle = state.alloc_handle();
|
const K_X_NOTIFY_LIVE: u64 = 0x00000002;
|
||||||
|
const K_X_NOTIFICATION_SYSTEM_UI: u32 = 0x09;
|
||||||
|
const K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED: u32 = 0x0A;
|
||||||
|
const K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED: u32 = 0x02000001;
|
||||||
|
const K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED: u32 = 0x02000003;
|
||||||
|
|
||||||
|
fn notification_mask_index(id: u32) -> u32 {
|
||||||
|
(id >> 25) & 0x3F
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notification_version(id: u32) -> u32 {
|
||||||
|
(id >> 16) & 0x1FF
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue_notification(obj: &mut KernelObject, id: u32, data: u32) {
|
||||||
|
if let KernelObject::NotifyListener { mask, max_version, queue, .. } = obj {
|
||||||
|
let idx = notification_mask_index(id);
|
||||||
|
if (*mask & (1u64 << idx)) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if notification_version(id) > *max_version {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.push_back((id, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_notify_create_listener(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
|
let mask = ctx.gpr[3];
|
||||||
|
let mut max_version = ctx.gpr[4] as u32;
|
||||||
|
if max_version > 10 {
|
||||||
|
max_version = 10;
|
||||||
|
}
|
||||||
|
let handle = state.alloc_handle_for(KernelObject::NotifyListener {
|
||||||
|
mask,
|
||||||
|
max_version,
|
||||||
|
queue: std::collections::VecDeque::new(),
|
||||||
|
waiters: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut startup_pending: Vec<(u32, u32)> = Vec::new();
|
||||||
|
if !state.has_notified_startup && (mask & K_X_NOTIFY_SYSTEM) != 0 {
|
||||||
|
state.has_notified_startup = true;
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_SYSTEM_UI, 0));
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1));
|
||||||
|
}
|
||||||
|
if !state.has_notified_live_startup && (mask & K_X_NOTIFY_LIVE) != 0 {
|
||||||
|
state.has_notified_live_startup = true;
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1));
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0));
|
||||||
|
}
|
||||||
|
if let Some(obj) = state.objects.get_mut(&handle) {
|
||||||
|
for (id, data) in startup_pending {
|
||||||
|
enqueue_notification(obj, id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = mem;
|
||||||
ctx.gpr[3] = handle as u64;
|
ctx.gpr[3] = handle as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
fn xnotify_get_next(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
// r3 = handle, r4 = id_ptr, r5 = param_ptr
|
let handle = ctx.gpr[3] as u32;
|
||||||
ctx.gpr[3] = 0; // FALSE (no notifications)
|
let match_id = ctx.gpr[4] as u32;
|
||||||
|
let id_ptr = ctx.gpr[5] as u32;
|
||||||
|
let param_ptr = ctx.gpr[6] as u32;
|
||||||
|
|
||||||
|
if param_ptr != 0 {
|
||||||
|
mem.write_u32(param_ptr, 0);
|
||||||
|
}
|
||||||
|
if id_ptr == 0 {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mem.write_u32(id_ptr, 0);
|
||||||
|
|
||||||
|
let Some(KernelObject::NotifyListener { queue, .. }) = state.objects.get_mut(&handle) else {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dequeued = if match_id != 0 {
|
||||||
|
let pos = queue.iter().position(|&(id, _)| id == match_id);
|
||||||
|
match pos {
|
||||||
|
Some(p) => {
|
||||||
|
let (id, data) = queue.remove(p).unwrap();
|
||||||
|
Some((id, data))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queue.pop_front()
|
||||||
|
};
|
||||||
|
|
||||||
|
match dequeued {
|
||||||
|
Some((id, data)) => {
|
||||||
|
mem.write_u32(id_ptr, id);
|
||||||
|
if param_ptr != 0 {
|
||||||
|
mem.write_u32(param_ptr, data);
|
||||||
|
}
|
||||||
|
ctx.gpr[3] = 1;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Session =====
|
// ===== Session =====
|
||||||
@@ -472,4 +571,99 @@ mod tests {
|
|||||||
xget_avpack(&mut ctx, &mem, &mut state);
|
xget_avpack(&mut ctx, &mem, &mut state);
|
||||||
assert_eq!(ctx.gpr[3], 8);
|
assert_eq!(ctx.gpr[3], 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drain_notifications(state: &mut KernelState, mem: &GuestMemory, handle: u32) -> Vec<(u32, u32)> {
|
||||||
|
let id_ptr = SCRATCH_BASE + 0x100;
|
||||||
|
let param_ptr = SCRATCH_BASE + 0x104;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
loop {
|
||||||
|
let mut ctx = PpcContext::default();
|
||||||
|
ctx.gpr[3] = handle as u64;
|
||||||
|
ctx.gpr[4] = 0;
|
||||||
|
ctx.gpr[5] = id_ptr as u64;
|
||||||
|
ctx.gpr[6] = param_ptr as u64;
|
||||||
|
xnotify_get_next(&mut ctx, mem, state);
|
||||||
|
if ctx.gpr[3] == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let id = mem.read_u32(id_ptr);
|
||||||
|
let data = mem.read_u32(param_ptr);
|
||||||
|
out.push((id, data));
|
||||||
|
if out.len() > 16 {
|
||||||
|
panic!("runaway dequeue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_listener(state: &mut KernelState, mem: &GuestMemory, mask: u64, max_version: u32) -> u32 {
|
||||||
|
let mut ctx = PpcContext::default();
|
||||||
|
ctx.gpr[3] = mask;
|
||||||
|
ctx.gpr[4] = max_version as u64;
|
||||||
|
xam_notify_create_listener(&mut ctx, mem, state);
|
||||||
|
ctx.gpr[3] as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_listener_with_full_mask_gets_4_startup_notifications() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
assert_eq!(
|
||||||
|
drained,
|
||||||
|
vec![
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_UI, 0),
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
|
||||||
|
(K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1),
|
||||||
|
(K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_listener_does_not_re_fire_startup() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h1 = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
let _ = drain_notifications(&mut state, &mem, h1);
|
||||||
|
let h2 = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
assert!(drain_notifications(&mut state, &mem, h2).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn system_only_mask_filters_live() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, K_X_NOTIFY_SYSTEM, 10);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
assert_eq!(
|
||||||
|
drained,
|
||||||
|
vec![
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_UI, 0),
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_filter_drops_too_new() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, 0x2F, 0);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
let kept_versions: Vec<u32> = drained.iter().map(|&(id, _)| notification_version(id)).collect();
|
||||||
|
assert!(kept_versions.iter().all(|&v| v == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xnotify_get_next_returns_zero_for_unknown_handle() {
|
||||||
|
let (mut ctx, mem, mut state) = fresh();
|
||||||
|
let id_ptr = SCRATCH_BASE + 0x100;
|
||||||
|
let param_ptr = SCRATCH_BASE + 0x104;
|
||||||
|
ctx.gpr[3] = 0xDEAD_BEEF;
|
||||||
|
ctx.gpr[4] = 0;
|
||||||
|
ctx.gpr[5] = id_ptr as u64;
|
||||||
|
ctx.gpr[6] = param_ptr as u64;
|
||||||
|
xnotify_get_next(&mut ctx, &mem, &mut state);
|
||||||
|
assert_eq!(ctx.gpr[3], 0);
|
||||||
|
assert_eq!(mem.read_u32(id_ptr), 0);
|
||||||
|
assert_eq!(mem.read_u32(param_ptr), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user