EMULATOR: why is SWMSG.SWPINFO zero when the swapper reads it? (2026-07-20)¶
Full path: E:\Dev\Ronny\NDInsight\SINTRAN\ND500\EMULATOR-SWPINFO-GAP-ANALYSIS-2026-07-20.md
Track: RetroCore ND-500 emulator (read-only static analysis; no code changed).
Question: the D4 swapper runs to PC=0x0800913B then null-derefs (w4 laddr r2.(10), r2=0)
because its work-message pointer SWMSG.SWPINFO (field HSWPI=0o104) reads zero. Which of the four
candidate causes is it, and what is the exact fix?
Grades: [V] = verified by direct static reading of the cited RetroCore source; [I] = inference
from that reading; [LIVE] = needs a live trace / harness output to settle. ASCII only; octal shown
as NNNB or 0oNNN, hex as 0xNN.
LIVE CORRECTION 2026-07-21 [V, D4 harness run _d4_swpinfo_run.txt] - the pointer is NOT zero¶
A live Nd500_D4_RunDomain_RealCpu_Capture run settled the "one load-bearing fact" below and
DISPROVES this doc's original premise that SWPINFO reads zero. Captured facts:
- The MON 377B RESTART write-back delivers a NON-ZERO SWPINFO pointer: @0x080240B4 := 0x00210718
(ND-100 word 0x210718 = byte 0x420E30 = MESSBUFF), with message-control @0x080240B0 := 0x00000005.
So SINTRAN DID answer with a valid pointer - the pointer is not the problem.
- What is zero is the MESSAGE BODY the pointer points at: RIOM src=0x00210718 dest=0x080240BC
count=15 reads ND-100 0x420E30 = 15 words of 00. The swapper then dispatches an EMPTY message
and null-derefs: MMU read protection violation at 0x0000000A PC=0x0800913B (r2=0 -> read addr 0x0A).
- Therefore the original "reads zero either way / SWPINFO pointer is zero" claim (old section 1.3)
is DELETED. The correct statement: the pointer is delivered fine; the MESSBUFF it addresses was
never populated with a real work message, because no real ND-500 domain activation exists (the
upstream D4 RUN-precondition blocker). The swapper is 3STARTed as a placement formality against an
empty MESSBUFF.
- Consequence for the fix (old section 4.2 option 1 is WRONG): a gate on SWMSG.SWPINFO == 0
would NEVER fire (the pointer is 0x210718, non-zero). And control=5 is NOT a "no work" sentinel to
gate on either - the Q-MMU-06 swapper-asm carve (memory nd500-d4-path-to-nll) shows control=5 =
fn code MSWIN ("init/activate working set", handler idx 5, the deepest paging worker), a
LEGITIMATE work reason that SINTRAN posts via 5ACTSWAPPER @144762B. So there is NO valid gate here.
- The real defect is upstream and OUT of this emulator: the requester's MESSBUFF body at
0x420E30 (a reused 200B / process-1 buffer that HSWPI points at) is empty because whoever POSTED the
MSWIN message never filled its body. That SENDER is OUTSIDE the carved NPL tree (placement /
segment-admin / init, possibly uncarved 030-S3SM5). [OPEN - the true next carve]: who posts the
MSWIN(fn5) message to the swapper, and why is its body empty in the D4 flow? Genuine "no work" is
the LNEWSWAP-EMPTY path (MP-P2-N500.NPL:1060) which zeroes HSWPI and does NOT restart the swapper -
never "reason 5 + zero buffer", so the empty body is an illegitimate upstream state, not a swapper
or emulator bug.
Everything below is the ORIGINAL 2026-07-20 static analysis, kept for its correct file:line chain but READ IT THROUGH THIS CORRECTION - wherever it says "SWPINFO is zero" read "the MESSBUFF body is zero; the pointer is 0x210718".
BOTTOM LINE (verdict) [ORIGINAL - superseded on the "pointer is zero" point by the LIVE CORRECTION above]¶
It is CAUSE 1 + CAUSE 3 (one mechanism), NOT cause 2 and NOT cause 4.
The emulator writes SWMSG.SWPINFO NOWHERE itself - it depends 100% on the REAL SINTRAN (running on
the emulated ND-100) to post it, via SINTRAN's answer to the swapper's MON 377B (delivered as a
24B/26B restart write-back). The swapper is being STARTED and driven into its message-dispatch loop
(the emulator runs the real swapper code on CpuND500) in a system state where SINTRAN has NOT posted
a real activating work message. So SINTRAN answers the swapper's MON 377B with SWPINFO = 0, the
RIOM pulls an empty message, and the dispatch null-derefs. This is exactly the carve's conclusion
(CARVE-SWAPPER-CONTEXT-BLOCK-BUILDER-2026-07-20.md section 6): the swapper is kicked without a real
page-faulting ND-500 process behind it, so SWMESS/5ACTSWAPPER's CNVWADR(activating-message)
yields zero.
- Cause 2 (address mismatch) is NOT the current failure. With
SWPINFO = 0theRIOMsource is0, not a mismatched0x460E30-vs-0x420E30. The_private = 0override (Nd500CpuProcessBridge.cs:75) is already correct; it only starts to matter AFTERSWPINFObecomes non-zero. So cause 2 is a LATENT downstream issue, not the stop at 0x913B.[V] - Cause 4 (CNVWADR / field offset) is NOT an emulator bug. The emulator performs no
CNVWADRand writes noSWPINFOfield.CNVWADRis SINTRAN's, on the ND-100 side.[V]
The single load-bearing fact still missing ([LIVE]): whether SINTRAN's SWPINFO is zero
already in SWMSG at 23B-start time (-> cause 1/3 confirmed, the start is premature/workless), or
is non-zero in SWMSG but arrives as zero through the MON 377B write-back path (-> would reopen
cause 2). The emulator can read this directly (see the fix) - it does not require the CPU round-trip.
1. The evidence chain, static, file:line¶
1.1 The emulator never posts SWPINFO - it relays SINTRAN [V]¶
Grepped the whole Emulated.HW\ND\CPU\ND500 tree for the SWPINFO/HSWPI field (word offset 0o104
inside SWMSG, byte 0x88; the 32-bit pointer spans HSWPI=0o104..SWPIN=0o105 per the carve). The
only hits are COMMENTS (Riom.cs:255/269/313/315, Nd500CpuProcessBridge.cs:474). No servicer or
bridge code ever writes message word 0o104. The servicer's message writes cover only
N5STA/SENDE/X5CPU/N500A(7)/N500A_LO(10B)/STOPR(11B)/NUMPA(12B)/MCNO(13B)/MSWMC(14B)/TRAPN(16B) plus
the 0x40/0x80 arg slots (Nd500MicrocodeServicer.cs:757-773, 891-903). Conclusion: the emulator
holds no copy of SWPINFO and fabricates none - it can only carry whatever SINTRAN produces. [V]
1.2 How the swapper actually obtains SWPINFO in the emulator [V]¶
Carved swapper MAIN (CARVE-SWAPPER-ENTRY-STARTUP-2026-07-20.md section 3.1): clears message-control
0x240B0/0x240B4 -> MON 377B sub-fn 1 (ask ND-100 for a message) -> h riom ... (DMA-pull it) ->
jumpg dispatch. In the emulator that MON 377B is intercepted at
Nd500CpuProcessBridge.OnMonitorCall (Nd500CpuProcessBridge.cs:457-491), which calls
servicer.AnswerMonitorCallStop (Nd500MicrocodeServicer.cs:746): it writes a MOCALL stop record and
answers the process message, PARKING the CPU. Real SINTRAN on the ND-100 then services MON 377B and
issues a 24B/26B restart. The restart's write-back is read by ReadRestartRecord
(Nd500MicrocodeServicer.cs:814-840) and applied by ApplyRestartWriteBack
(Nd500CpuProcessBridge.cs:383-412), which does cpu.WriteVirtualMemory32(addr, value) into the
swapper's DSEG cell 0x240B4. RIOM then reads operand 0's VALUE = that cell = SWPINFO
(Riom.cs:258) and copies from ND-100 physical SWPINFO*2 (Riom.cs:408,423;
CpuND500.ND100Bridge.cs:82-91). So the SWPINFO the swapper sees is precisely SINTRAN's MON 377B
answer value. [V]
1.3 It is observed zero [V, from the code's own note]¶
Nd500CpuProcessBridge.cs:66-74 (the _private derivation comment) states plainly: the 0x210718 ->
0x420E30 agreement "has also never been observably confirmed, because the source reads zero either way
in the runs so far." That is the emulator author recording that SINTRAN's MON 377B write-back
delivers zero. The D4 harness instruments exactly this: _bridge.MonCallLog, the RESTART write-back
lines, and the side-by-side dump of DSEG+0x240BC vs ND-100 0x420E30
(Nd100SintranNd500BootHarnessTests.cs:1101-1121). [V]
1.4 Why SINTRAN's SWPINFO is legitimately zero [I, from carve]¶
SWMSG.SWPINFO = CNVWADR(activating-message-address), set by SWMESS (133654) or 5ACTSWAPPER
(145006) - and ONLY set when a real activating process/message exists
(CARVE-SWAPPER-CONTEXT-BLOCK-BUILDER-2026-07-20.md sections 1, 6). In the D4 flow the swapper is
brought up as a placement/bring-up formality, with no page-faulting ND-500 domain process behind it,
so there is no activating message to convert -> SWPINFO stays zero. The emulator's own machinery
runs the swapper CPU into its dispatch loop regardless (see section 2), so the zero becomes a
null-deref instead of a benign "no work, go back to sleep". [I]
2. The emulator-side trigger (why the swapper runs at all with no work)¶
Nd500CpuProcessBridge.OnStartProcess (Nd500CpuProcessBridge.cs:111-160) runs the real swapper on
CpuND500 whenever a 21B register image was stashed (servicer.ProcessZeroRegisterImage != null,
line 126) and a 23B 3START arrives (Nd500MicrocodeServicer.cs:515-544). It then installs the MMU
mapping (InstallSwapperMapping, line 147-151) and wakes the run thread (line 156-157). Nowhere in
this path does it check that the activating SWMSG carries a non-zero SWPINFO - i.e. that SINTRAN
has real work for the swapper. This is the "functional swapper" hack (comment lines 134-146): it runs
the swapper's macro-code to make it do real work, but there is no gate ensuring there IS work. [V]
Note also the currently-UNUSED AnnounceSwapperAlive (Nd500MicrocodeServicer.cs:797-804): an older
"announce alive without running the CPU" path (synthesizes the MON 377B self-announce, VALUE=1). It
is defined but not called anywhere (grep: only the definition). The current design deliberately runs
the real CPU instead - which is exactly why an empty activation now crashes rather than being absorbed
by the synthetic announce. [V]
3. What is CERTAIN vs what needs a LIVE trace¶
Certain from static reading:
- The emulator never sources SWPINFO itself; it relays SINTRAN's MON 377B write-back. (Rules out
cause 4 as an emulator bug.) [V]
- With SWPINFO = 0, the RIOM source is 0; the _private value is irrelevant to the current stop.
(Rules out cause 2 as the CURRENT failure.) [V]
- OnStartProcess drives the swapper into its dispatch loop with no check that a real work message
exists. [V]
Needs a LIVE trace to finish the proof (one value):
- Read SWMSG.SWPINFO = (ReadNd100Word(msgBase + 0o104*2) << 16) | ReadNd100Word(msgBase + 0o105*2)
at the moment the 23B 3START is dispatched (Nd500MicrocodeServicer.cs:515). msgBase for the
swapper start IS SWMSG (the activation message). This single value settles it:
- SWPINFO == 0 at 23B time -> CONFIRMS cause 1/3: SINTRAN posted no work; the start is
premature/workless. (Expected, given section 1.3.)
- SWPINFO != 0 at 23B time but the MON 377B write-back still delivers 0 -> REOPENS cause 2: the
write-back address/routing is wrong. (Contradicted by section 1.3's "reads zero either way", but
only a direct read excludes it.)
- The D4 harness MonCallLog + RESTART write-back lines already capture the MON 377B side; add the
23B-time SWPINFO read to close the loop. This does not require the CPU to run.
4. Proposed fix¶
4.1 Step 0 - the decisive instrumentation (do first, zero risk) [CERTAIN action]¶
File: E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\ND\CPU\ND500\Servicer\Nd500MicrocodeServicer.cs
Method: the StartProcess/TrapContinue case at line 515-544, right before it calls
ProcessHost.OnStartProcess(msgBase, micfu) (line 542).
Read and log SWMSG.SWPINFO from the activating message:
// HSWPI = 0o104 word offset inside SWMSG; SWPINFO is the 32-bit pointer at 0o104..0o105
// (CARVE-SWAPPER-CONTEXT-BLOCK-BUILDER-2026-07-20.md sec 1). This is the value SINTRAN will
// later hand the swapper for its MON 377B / RIOM. Reading it here says WHETHER SINTRAN posted
// a real work message BEFORE the swapper is ever run - the whole SWPINFO=0 question.
uint swpinfo = ((uint)host.ReadNd100Word(msgBase + (0o104 * 2)) << 16)
| host.ReadNd100Word(msgBase + (0o105 * 2));
host.ServicerLog($"MAILBOX 23B 3START @word 0x{msgBase >> 1:X6}: SWMSG.SWPINFO=0x{swpinfo:X8}");
This is a pure read (no behaviour change) and produces the one fact needed to lock the verdict.
4.2 Step 1 - the actual fix, branch-selected by Step 0's value¶
- If
SWPINFO == 0at 23B time (expected): the swapper is being started with no work. The correct behaviour is NOT to drive the real dispatch loop into a null message. Two viable fixes; pick per how faithfully SINTRAN's own path should be reproduced:- Gate the run (smallest): in
Nd500CpuProcessBridge.OnStartProcess(Nd500CpuProcessBridge.cs:111), after loading the register image / installing the mapping, do NOT wake the run thread when the activatingSWMSG.SWPINFO == 0; leave the CPU parked (WAIT) so it "sleeps" until a later 5ACTSWAPPER activation carries a realSWPINFO. This mirrors real hardware, where the swapper'sMON 377Bblocks until SINTRAN has a message. - Restore the aliveness-only announce: for a workless START-SWAPPER, use
AnnounceSwapperAlive(Nd500MicrocodeServicer.cs:797) to satisfy SINTRAN's "swapper free" handshake WITHOUT running the CPU, and only run the realCpuND500for activations that actually carrySWPINFO != 0. This restores the previously-working "alive but idle" state and confines real execution to real work. Recommended: option 1 (keeps the real-CPU path, adds one guard, does not resurrect dead code). The deeper upstream question - why does SINTRAN never post a real activation on the RUN path - is the pre-existing D4 RUN-precondition blocker and is out of scope for this null-deref fix; option 1 stops the crash and leaves the swapper correctly idle until that upstream work lands.
- Gate the run (smallest): in
- If
SWPINFO != 0at 23B time (only if Step 0 surprises us): the bug is in delivery, i.e. cause- Fix the
MON 377Banswer/write-back so the non-zero pointer reaches DSEG0x240B4, and re-check the_private/MapND100ToPhysicalmapping (CpuND500.ND100Bridge.cs:82-91) soRIOM's source resolves to the 5MPM message buffer.
- Fix the
5. Regression risk to the currently-working run-to-0x913B¶
- Step 0 (the read/log) is a pure read of two ND-100 words at the existing
msgBase; it cannot change any behaviour. Zero regression risk.[V] - Step 1 option 1 (park instead of wake when SWPINFO==0) changes behaviour ONLY on the workless
start that currently crashes - the swapper no longer reaches 0x913B, which is the point. It does NOT
touch the path where
SWPINFO != 0, so a genuine work activation still runs exactly as today. The one risk: if some OTHER consumer relies on the swapper always reaching 0x913B (e.g. a test asserting the null-deref trap), it would change - but that trap is the bug, not a contract. Confirm no test asserts the 0x913B trap as success before landing.[I] - Step 1 option 2 (announce-alive) is higher risk: it reintroduces a code path
(
AnnounceSwapperAlive) that was deliberately parked, and splits swapper handling into two modes; more surface for divergence from SINTRAN's real handshake. Prefer option 1.[I] - Do NOT change the
_private = 0override (Nd500CpuProcessBridge.cs:75) as part of this fix - it is correct for this path and only bites onceSWPINFOis non-zero (open question C4 inOPEN-QUESTIONS-REGISTER-2026-07-20.md).[V]
6. Evidence index¶
| Claim | Source (RetroCore unless noted) | Grade |
|---|---|---|
| Emulator writes no SWPINFO field; relays SINTRAN | grep Emulated.HW\ND\CPU\ND500; Nd500MicrocodeServicer.cs:757-773 |
[V] |
| SWPINFO delivered via MON 377B -> 24B/26B write-back | Nd500CpuProcessBridge.cs:457-491,383-412; Nd500MicrocodeServicer.cs:746,814-840 |
[V] |
| RIOM source = operand0 value = the DSEG SWPINFO cell | Instructions/IO/Riom.cs:258,408,423 |
[V] |
| Source observed zero in runs so far | Nd500CpuProcessBridge.cs:66-74 (author note) |
[V] |
| OnStartProcess runs swapper with no work-message gate | Nd500CpuProcessBridge.cs:111-160; Nd500MicrocodeServicer.cs:515-544 |
[V] |
| _private=0 override correct; latent, not current cause | Nd500CpuProcessBridge.cs:75; CpuND500.ND100Bridge.cs:48,82-91 |
[V] |
| AnnounceSwapperAlive defined but uncalled | grep -> only Nd500MicrocodeServicer.cs:797 |
[V] |
| SWPINFO=CNVWADR(activating msg), only set with real work | CARVE-SWAPPER-CONTEXT-BLOCK-BUILDER-2026-07-20.md sec 1,6 |
[I] |
| HSWPI=0o104, SWPIN=0o105 (32-bit pointer) | same carve, L07 symbol pins | [V, carve] |
| 23B-time SWPINFO value (the remaining discriminator) | needs live D4 run / Step-0 log | [LIVE] |
File written: E:\Dev\Ronny\NDInsight\SINTRAN\ND500\EMULATOR-SWPINFO-GAP-ANALYSIS-2026-07-20.md