Skip to content

PowerPC: little-endian mode on the 604, and Bandit byte-lane reversal - #135

Open
pappadf wants to merge 4 commits into
mainfrom
ppc-le-mode-and-bandit-lane-reversal
Open

PowerPC: little-endian mode on the 604, and Bandit byte-lane reversal#135
pappadf wants to merge 4 commits into
mainfrom
ppc-le-mode-and-bandit-lane-reversal

Conversation

@pappadf

@pappadf pappadf commented Sep 5, 2026

Copy link
Copy Markdown
Owner

What this changes

Two related gaps that together prevented the machine from running any
little-endian PowerPC client.

1. MSR[ILE] / MSR[LE] on the 604

Both bits were masked out of the MSR, so mtmsr/rfi silently dropped them and
the core always ran big-endian. They are implemented on the 604 only; the 601
mask is unchanged.

A 60x in little-endian mode does not reorder bytes. It munges the low three bits
of every effective address (XOR 7/6/4 for byte/halfword/word, nothing for an
aligned doubleword), and instruction fetch munges like a word load. In
src/core/cpu/ppc/:

  • ppc_le_ea() applies the munge. ea stays architected everywhere an
    instruction writes it back or reports it.
  • Instruction fetch XORs the pc with a cached le_xor, recomputed on every
    fetch-window refill. A stale value cannot survive an MSR write, which flushes
    the window.
  • Any misaligned scalar access, and every lmw/stmw/lswi/lswx/stswi/
    stswx, takes the alignment exception.
  • A doubleword FP transfer becomes two munged word accesses, high-order word at
    the higher address.
  • Exception entry replaces MSR[LE] with a copy of MSR[ILE]; rfi restores
    LE from SRR1.
  • debug.disasm applies the fetch munge so little-endian code lists in program
    order.

2. Bandit byte-lane reversal

The address munge is only half the mechanism. The host bridge carries the other
half: mode-select $50 bit 24 is the endian bit, set at power-on for the
straight byte lanes every Macintosh OS uses, and cleared by the firmware for a
little-endian client. The CPU munge and the bridge reversal cancel, so a
little-endian guest reaches PCI with plain loads and stores.

  • pci.c/pci.h gain a per-bus flag (pci_bus_set_lane_reverse /
    pci_bus_lane_reverse). The window dispatch applies it once, before decode:
    an N-byte access at offset o becomes o ^ (8-N) with its bytes reversed, so
    no device model sees anything but PCI byte addresses.
  • bandit.c drives the flag from mode-select, and the bridge's own config
    address and data ports honour it too
    . That part is required. The firmware's
    set-caddr composes a natural one-hot config address and stores it with
    xl!, and xl@/xl! are the byte-flipping variants only in big-endian mode
    (the ROM patches those tokens to the flipping forms under little? 0=). In
    little-endian mode they are plain, so the flip has to come from the bridge.
  • Anything that bypasses the CPU applies the same reversal itself: the Grand
    Central island's direct mapping and the DBDMA movers (tnt.c), and the 53C825
    SCRIPTS bus-master block movers (scripts53c8xx.c).

Why the config ports matter

With straight config ports the firmware's own probe reads $00100300 where it
compares against $31000. Its Shiner-ESB test therefore fails, and the hook
that installs the two 53C825 controllers and the 54M30 video never creates those
nodes. Observed device tree in little-endian mode, before and after:

before                          after
/bandit@F2000000                /bandit@F2000000
  /gc@10                          /53c825@11
  /pci106b,1@B                    /53c825@12
                                  /gc@10
                                  /pci106b,1@B
                                  /54m30@F

After the change the little-endian tree matches the big-endian tree exactly, and
the firmware's ?esb reads -1 instead of 0.

How far this takes the boot

Testing used the 2.26NT ANS ROM and the Windows NT 4.0 PowerPC install media.
Neither is in this repository.

1. Open Firmware boots little-endian and keeps the serial console. After
setting little-endian? true, real-mode? false, real-base and load-base
and restarting, the console comes back on ttya:

disk2:aix   can't OPEN: /bandit/53c825@11/sd@2,0:aix
OpenFirmware2.26
To continue booting from the default boot device type:
BOOT<return>
 ok
0 >

The fully expanded device path in that message is itself evidence the 53C825
node now exists; before the change the same line read can't OPEN: disk2:aix.

2. The NT veneer loads and lays out. Reading it off a SCSI disk with
read-blocks and relocating it produces:

Loading PE/COFF
image_base  50000

A full byte comparison of guest memory 0x50000..0x63000 against the expected
laid-out image (comparing after un-reversing aligned 8-byte groups, since a
little-endian guest stores it doubleword-reversed) gives:

section range mismatches
.text 0x50000-0x5C400 0
.bss 0x5C400-0x5CC00 0
.rdata 0x5CC00-0x60C00 0
.data 0x60C00-0x61C00 0
.reldata 0x61C00-0x61E00 0

3. The veneer executes. Single-stepping its entry:

50000  bl   50008           lr=00050004
50008  mflr r2              r2=00050004
5000c  lwz  r2,0(r2)        r2=000691b8   (TOC pointer)
50010  lwz  r8,-30588(r2)   r8=00060c00   (into .data)
50014  stw  r5,0(r8)                      (no fault)
50024  bctrl            ->  00050068

The TinkerDifferent thread records a hardware failure at exactly 0x50014, with
r8 holding garbage (18A2D8C5) because the veneer's .data was not loaded.
That does not reproduce here: .data is loaded correctly, r8 resolves into
it, and the store succeeds.

4. The ARC interface comes up. Console output, matching the banner recorded
in that thread:

Open Firmware ARC Interface  Version 3.0 (Jul 12 1996 - 18:46:28)
Couldn't claim SYSTEM PARAMETER BLOCK
Program complete - please reboot.
EXIT called

The failing call is call-method("claim", ihandle=/memory, align=0, size=0xD0, virt=0x4000) returning -1. Mapping the page with map-space does not satisfy
it, because mapping is not claiming.

5. With the thread's two documented nop patches applied (they nop the bl
to the claim wrapper, so r3 keeps the li r3,0x4000 immediately before it,
which is not -1, and the error branch is skipped), the veneer gets past both
claims and reaches SETUPLDR:

Open Firmware ARC Interface  Version 3.0 (Jul 12 1996 - 18:46:28)
OFClose(ff8d3a80) failed
OFClose(ff8d39c0) failed
OFClose(ff8d3840) failed
Booting from 'device-tree(0)partition(1)\PPC\SETUPLDR'
VrOpen returned d
Program complete - please reboot.
EXIT called

That is as far as it goes. Windows NT Setup and the HAL are not reached.

The next blocker, which this PR does not introduce

Open Firmware's ISO-9660 file read returns wrong data past exactly 16 KB.
Loading a 0x27800-byte file off the CD is byte-perfect for the first 0x4000
bytes and then diverges. The data it fetches comes from
LBA = expected + file_start_LBA, i.e. the extent base is added a second time
(expected 115061, actual 230114, extent start 115053).

Raw read-blocks of 16 CD blocks at the same LBA is byte-perfect, so the CD
model and the 53C825 DMA path (including the new lane reversal) are correct.

This reproduces identically in big-endian mode — same 7919 mismatching bytes
at the same 0x4000 boundary — so it is pre-existing and unrelated to this
change. It is, however, what now blocks loading SETUPLDR (400 KB), and it is the
next thing to fix.

Correction (2026-09-05, later the same day): the defect above is real but it
is the firmware's own behaviour, and it was not what blocked SETUPLDR. See the
follow-up section below.

Follow-up, 2026-09-05: SETUPLDR runs and the emulator reaches the real ANS 700's screen

Same branch, no further code change. Driven interactively from a checkpoint taken
just before go, with the veneer's own debug trace switched on (it has a VrDebug
bitmask in .data and ships with its COFF symbol table, so every routine has a name).
Three findings, each read off the 2.26NT firmware's detokenized Forth source (joevt's
package, thread post 43745) and confirmed against the trace.

1. The 16 KB ISO-9660 read defect is the firmware's, and the veneer never hits it.
The deblocker support package buffers 8 × 2 KB blocks. iso-9660-files open seeks
the deblocker to the file's absolute extent while open-ok? is still false, then
sets open-ok?, after which its read-blocks adds fileextent to every block number
the deblocker asks for. The first 16 KB come out of the buffer already filled, and the
next request is for block fileextent + 8, which becomes LBA 2·fileextent + 8
exactly the numbers above. The firmware's own $load calls the package's load
without a seek, so a bare load of any ISO file over 16 KB is wrong on real hardware
too. The NT veneer issues seek before every read, which re-positions the deblocker
relative to the file, so this never affects the NT path.

2. VrOpen returned d (ARC ENODEV) was an empty /chosen bootpath. The thread's
transcript loads the veneer by hand with read-blocks, so nothing sets bootpath
only the firmware's load/boot do. The veneer's find_boot_dev reads that property,
finddevice("") yields the root node, whose ARC name is the Open Firmware root's
name, device-tree, and VrOpen then cannot find a device-tree(0) component
under the ARC root. Setting the property at the prompt before go

" /bandit/53c825@11/sd@0,0" encode-string " bootpath" _chosen (property)

makes the veneer derive multi(0)scsi(0)cdrom(0)fdisk(0)partition(1)\PPC\SETUPLDR,
open it, relocate SETUPLDR's six sections, build the memory descriptor list and jump
to it. That is the path named in the real machine's error message (post 49785), so the
real ANS 700 had bootpath set as well.

3. The real hardware's wall, The file multi(0)scsi(0)cdrom(0)fdisk(0)partition(1) is corrupted, is reproduced verbatim, and its cause is the firmware's disk-label.
SETUPLDR opens the raw partition (sd@0,0:1) and probes it for a filesystem: reads of
0x62 bytes at 0, 512 at 0x2000, 528 at 0 and 2048 at 0x8000 (the ISO volume
descriptor). Apple's disk-label open, given the argument 1 with no file name,
still detects CD001 and interposes iso-9660-files, whose open with no path leaves
the instance on the ISO root directory as a pseudo-file (428 bytes on this CD).
seek does not update its fileposn and read clamps to filesize, so the four
reads returned 98, 330, 0 and 0 bytes — 428 − 98 = 330 — and the descriptor read gets
nothing. SETUPLDR's recognizers all fail and it reports the partition corrupted. The
console then shows

Windows NT Setup
The file multi(0)scsi(0)cdrom(0)fdisk(0)partition(1) is corrupted.
Press any key to continue.

which is the furthest screen anyone has reached on real hardware (post 49785).

Where that leaves the emulator: level with the real ANS 700, with the little-endian
CPU mode and Bandit lane reversal from this PR carrying the veneer, the firmware's
client interface and SETUPLDR through several hundred million instructions without a
CPU- or bus-side fault. Nothing in the two walls points at the emulator. Going further
means giving SETUPLDR raw sector access to the CD (a firmware method override, or a
veneer patch), after which text-mode Setup is expected to ask for the computer type —
the missing ANS HAL. That is deliberately not part of this PR.

Follow-up 2, 2026-09-05: past the real machine's wall — SETUPLDR stops at the HAL menu

Still no code change on this branch; the emulator was used to find out why SETUPLDR
says the partition is corrupted, and the answer is two bytes in the veneer.

The "corrupted" message is BlGenerateDeviceNames rejecting the boot name. With
raw partition reads made to work (a disk-label open override typed at the firmware
prompt), SETUPLDR's own CDFS read the volume descriptor, the root and PPC directories
and the first 1 KB of its own image — and still printed the message. Breakpoints on its
error reporters caught SlFriendlyError(EINVAL, "multi(0)scsi(0)cdrom(0)fdisk(0)partition(1)", setup.c:416)
right after BlGenerateDeviceNames, which parses ARC names lexically and requires
cdrom(N)fdisk(N) to be the end of the name; the trailing partition(1) is EINVAL,
and SlFriendlyError renders EINVAL as "The file %s is corrupted". The veneer's
find_boot_dev appends partition(1) to every boot path, correct for a hard disk and
wrong for a CD-ROM. Same veneer, same SETUPLDR on the real ANS 700, so this is the
hardware thread's wall too, independent of the firmware.

Two one-byte patches to VENEER.EXE, no firmware change:

image file .rdata string new value effect
0x5D0C0 0xD2C0 "partition(1)" "" boot path becomes multi(0)scsi(0)cdrom(0)fdisk(0)\PPC\SETUPLDR
0x5E168 0xE368 ":0" "" VrOpen of the bare device passes an empty Open Firmware argument, the one case in which Apple's disk-label returns raw sector access instead of interposing iso-9660-files on the ISO root directory

With those and bootpath set, SETUPLDR reads TXTSETUP.SIF, prints Setup is loading
files (Windows NT Executive)…
and stops at

Setup could not determine the type of computer you have, or you have
chosen to manually specify the computer type.

  IBM Power Series 6015 / 6020,40,42 / 6050,6070 and RS/6000 Model 7248
  IBM RS/6000 Model E20/E30/F30 / 45M/H45
  MOTOROLA PowerStack / PowerStack2 / Big Bend
  Powerized ES, MX, LX, TX (Uniprocessor) / (Multiprocessor)
  Other

which is the missing Apple Network Server HAL — the point this whole exercise was
aiming for. Everything on the way ran on the 604 little-endian mode and Bandit lane
reversal from this PR: the veneer, the firmware's client interface in little-endian
mode, SETUPLDR's CDFS over ARC reads, and its INF parser over the 126 KB TXTSETUP.SIF,
several billion instructions with no CPU- or bus-side fault.

Follow-up 3, 2026-09-05: the screen, the recipe, and what is loaded

Windows NT Setup on the emulated Apple Network Server asking for the computer type

Rendered from the ttya byte stream: SETUPLDR drives the ARC console with 8-bit CSI
cursor addressing, SGR colours (0m 37m 44m for the client area, 0m 30m 47m for the
bars) and CP437 box characters. The two OFClose(...) failed lines at the top are the
veneer's, printed before SETUPLDR painted the screen.

Where NT stands at this screen. The SCSI command log shows SETUPLDR read the whole
of \PPC\NTKRNLMP.EXE (84 READ commands over its 666 blocks) after TXTSETUP.SIF, so the
kernel image is in memory — but nothing of NT executes. The kernel imports all of its
hardware access from HAL.DLL, the loader binds those imports only after a HAL is chosen,
and none of the seven HALs on the CD is for an Apple machine. Control is in SETUPLDR's
menu loop, polling the ARC console for a key.

How to get here — the full recipe is in
docs/notes/2026-09-05-ans-windows-nt-setupldr.md
on this branch, with the mechanism of each wall. In short, on top of the thread's
transcript (post 49785: LE reboot, VENEER.EXE via read-blocks + init-program, the
four pokes), type at the 0 > prompt before go:

" /bandit/53c825@11/sd@0,0" encode-string " bootpath" _chosen (property)   \ the NT CD
00 5D0C0 c!                                                             \ veneer: "partition(1)" -> ""
00 5E168 c!                                                             \ veneer: ":0" -> ""
go

The first line is only needed when the veneer is hand-loaded (a boot from the CD sets
bootpath itself); the two bytes are 0xD2C0 and 0xE368 in the file for a static
VENEER.EXE patch. On the emulator it is make headless, the ANS 500 profile with
64 MB, the 2.26NT ROM, the NT 4.0 OEM CD on /bandit/53c825@11/sd@0,0 and the veneer
on a disk at /bandit/53c825@12/sd@0,0; the whole sequence from a cold boot to the menu
runs in about ten minutes at --speed=turbo.

The branch gained only that note and the image (commit 8ebed27); no code changed.

Testing

Added:

  • tests/unit/suites/ppc: test_604_little_endian covers the munge at every
    offset and width, update forms writing back the architected EA, lwbrx/
    lhbrx, aligned and word-aligned lfd/stfd, the alignment faults,
    ILE->LE on exception entry with rfi restoring it, and the 601 masking
    both bits off.
  • tests/unit/suites/pci: test_window_lane_reversal pins the offset transform
    and byte reversal per width, and that the flag reads back and is reversible.
  • tests/integration/tnt-pci-slots: clearing the endian bit reverses the
    pass-through island's lanes and the bridge's own config ports — a
    reversed 32-bit access then reads and writes the little-endian config
    dword as-is (mode-select reads $00000040; restoring the bit is a write
    of $01000040) — and restoring it restores the straight view.

Changed: that row's coherency handshake now writes 0x40000001 instead of
0x40000000, preserving bit 24. That is what the documented read / OR $40 /
write sequence does; the previous literal cleared the endian bit as a side
effect.

Run locally before pushing: make headless clean, and clang-format-18 --dry-run --Werror over the changed sources. The unit and integration suites
are left to CI.

CI follow-up (39fa333): the first CI run failed on exactly one row,
tnt-pci-slots, whose endian-bit block still asserted the earlier
"config ports stay straight" design. The test, the bandit.c header comment
and docs/machines/tnt/tnt.md now match the implemented behaviour described
above. No code change.

🤖 Generated with Claude Code

Apple's 2.26NT Open Firmware for the Apple Network Server runs the 604 in
little-endian mode (`little-endian? true`), and the PowerPC Windows NT
loaders are little-endian PE images. Two pieces were missing to execute
that path.

1. MSR[ILE] / MSR[LE] on the 604
--------------------------------
Both bits were masked out of the MSR, so mtmsr/rfi silently dropped them
and the core always ran big-endian. They are now implemented on the 604
only; the 601 mask is unchanged.

A 60x in little-endian mode does not reorder bytes. It munges the low
three bits of every effective address (XOR 7/6/4 for byte/halfword/word,
nothing for an aligned doubleword), and instruction fetch munges like a
word load. Implemented in src/core/cpu/ppc/:

  * ppc_le_ea() applies the munge; `ea` stays architected everywhere an
    instruction writes it back or reports it.
  * Instruction fetch XORs the pc with a cached le_xor, recomputed on
    every fetch-window refill (a stale value cannot survive an MSR write,
    which flushes the window).
  * Any misaligned scalar access, and every lmw/stmw/lswi/lswx/stswi/
    stswx, takes the alignment exception (604UM 4.5.6).
  * A doubleword FP transfer becomes two munged word accesses with the
    high-order word at the higher address.
  * Exception entry replaces MSR[LE] with a copy of MSR[ILE]; rfi
    restores LE from SRR1.
  * debug.disasm applies the fetch munge so little-endian code lists in
    program order.

2. Bandit byte-lane reversal
----------------------------
The CPU munge is only half of the mechanism; the host bridge carries the
other half. Mode-select $50 bit 24 is the endian bit: set at power-on for
the straight byte lanes every Macintosh OS uses, cleared by the firmware
for a little-endian client, at which point the bridge reverses its eight
byte lanes. The two cancel, so a little-endian guest reaches PCI with
plain loads and stores.

  * pci.c/pci.h gain a per-bus flag (pci_bus_set_lane_reverse /
    pci_bus_lane_reverse). The window dispatch applies it once, before
    decode: an N-byte access at offset o becomes o ^ (8-N) with its bytes
    reversed, so no device model sees anything but PCI byte addresses.
  * bandit.c drives the flag from mode-select, and the bridge's own
    config address and data ports honour it too. That last part is
    required: the firmware's `set-caddr` composes a natural one-hot
    config address and stores it with `xl!`, and `xl@`/`xl!` are the
    byte-flipping variants only in big-endian mode (the ROM patches those
    tokens to the flipping forms under `little? 0=`). In little-endian
    mode they are plain, so the flip has to come from the bridge.
  * Anything that bypasses the CPU applies the same reversal itself: the
    Grand Central island's direct mapping and the DBDMA movers (tnt.c),
    and the 53C825 SCRIPTS bus-master block movers (scripts53c8xx.c).

With straight config ports the firmware's own probe reads $00100300
where it compares against $31000, so its Shiner-ESB test fails and the
two 53C825 nodes and the 54M30 video are never created. With the
reversal the little-endian device tree matches the big-endian one.

Tests
-----
  * tests/unit/suites/ppc: test_604_little_endian covers the munge at
    every offset and width, update forms writing back the architected EA,
    lwbrx/lhbrx, aligned and word-aligned lfd/stfd, the alignment faults,
    ILE->LE on exception entry with rfi restoring it, and the 601 masking
    both bits off.
  * tests/unit/suites/pci: test_window_lane_reversal pins the offset
    transform and the byte reversal per width, and that the flag reads
    back and is reversible.
  * tests/integration/tnt-pci-slots gains coverage that clearing the
    endian bit reverses the pass-through island's lanes while the config
    ports stay straight, and that restoring it restores the straight
    view. Its coherency handshake now preserves bit 24, which is what the
    documented read/OR-$40/write sequence does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pappadf
pappadf force-pushed the ppc-le-mode-and-bandit-lane-reversal branch from 9a7e92e to 96a7336 Compare September 5, 2026 17:20
pappadf and others added 3 commits September 5, 2026 19:00
The row's endian-bit block still encoded an earlier design in which
Bandit's own config address and data ports stayed straight while the
lanes reversed.  The shipped model reverses the ports too (bandit.c,
port_offset / port_val32), because the firmware's `xl!`/`xl@` are the
byte-flipping variants only in big-endian mode, so in little-endian mode
the flip has to come from the bridge.  CI failed on the first assertion
of that block.

Seen from the big-endian script, a reversed 32-bit access reads and
writes the little-endian config dword as-is: after clearing the endian
bit mode-select reads $00000040, and restoring it means writing
$01000040, not $40000001 (which left the bridge reversed).  The test now
asserts exactly that, and checks both the pass-through island and the
data port after the restore.

Also brings the two prose statements that said the ports stay straight
into line with the code: the bandit.c header comment and
docs/machines/tnt/tnt.md.  No behavioural change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An investigation note (docs/notes, force-added like the existing notes there)
with the two walls between the thread's transcript and Windows NT Setup's
computer-type prompt, and the recipe that passes them: /chosen bootpath set
to the CD, and two one-byte patches to VENEER.EXE (image 0x5D0C0
"partition(1)" -> "", 0x5E168 ":0" -> "") so that SETUPLDR's
BlGenerateDeviceNames accepts the CD-ROM ARC name and Apple's disk-label
returns raw sector access. The screen, rendered from the ttya stream, goes
in docs/assets. No code change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Open Firmware and AIX arrive knowing what this chip is, so a flat byte array
per register block carried the model this far.  A driver that has to *find*
the part does not: Windows NT's cirrus.sys unlocks the extensions, reads the
chip ID, and sizes the framebuffer, and every one of those reads came back
as whatever had last been written.

  SR06  "Unlock ALL Extensions" — $12 unlocks and reads back $12, anything
        else locks and reads back $0F.  The round trip is the presence test.
  CR27  read-only $A0: CL-GD5430, revision 0 — the die this board carries,
        and the value that agrees with its $00A0 PCI device ID.  cirrus.sys
        requires CR27 >> 2 to fall in [$0B, $2F] before it goes on.
  SR15  "DRAM Control" bits 3:0 report the fitted memory; 2 is 1 MB.  The
        driver builds its mode list from this field rather than probing, so
        zero meant every mode was too large and the adapter had none.
  $3CC  Miscellaneous Output's read address, answering with what $3C2 was
        given.  Bit 0 says which CRTC pair to use, and a driver that read
        zero went to $3B4/$3B5 — which decoded nothing.  Both pairs now
        answer, so software that never writes $3C2 still works.

With these NT 4.0's cirrus.sys identifies the adapter and initialises it.
ans-console, ans-device-tree, ans-pci-slots, tnt-pci-slots, ans-aix-boot,
ans-macos-2rom and ans-aix-installed-boot all still pass — the goldens are
unchanged, because everything that already worked wrote $3C2 and SR06 the
way real VGA code does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant