# PPC64 JIT support for Firefox 154+
<img width="794" height="261" alt="trung…" src="https://github.com/user-attachments/assets/52623c36-2bfa-4c0e-b982-7a62d0d2e3c6" />
Brings JIT compilation to PPC64 — both **little-endian** (ppc64le) and **big-endian** (ppc64/ppc64be, ELFv1 and ELFv2) — targeting **POWER8+** (ISA v2.07+) with **POWER9** (ISA v3.0) / **POWER10** (ISA 3.1) optimizations. Enables SpiderMonkey's Baseline interpreter, Baseline and Ion JIT compilers, the native regexp JIT, plus full WebAssembly support — tiering, **SIMD**, **huge memory**, **JSPI**, tail calls, and atomics.
This PR revives the JIT port https://github.com/chmeeedalf/gecko-dev by Cameron Kaiser (classilla) and Justin Hibbits (chmeeedalf).
This PR would always track against the latest upstream. If you are a packager, please find downstream patches in [Releases](https://github.com/runlevel5/firefox/releases), if the version you are after are not there, please simply leave a comment in this PR and I will prepare the patches for you.
If you are after the ESR 153 support, please check out https://github.com/runlevel5/firefox-ppc64/pull/2
## Test results
> In tests, we trust!
**Real little-endian POWER8 hardware** (SpiderMonkey with debug, GCC 14, sponsored by Oregon State University):
| Suite | POWER8 default |
|---|---|
| Full jit-tests (`--jitflags=all`) | **13,715 / 0** |
| Full `jstests` (JSPI on) | **PASSED ALL** |
**Real little-endian POWER9 hardware** (SpiderMonkey with debug, GCC 16, sponsored by [Raptor Computing System](https://raptorcs.com)):
| Suite | POWER9 default | `MOZ_PPC64_FORCE_POWER8=1` |
|---|---|---|
| Full jit-tests (`--jitflags=all`) | **13,715 / 0** | **13,715 / 0** |
| Full `jstests` (JSPI on) | **PASSED ALL** | **PASSED ALL** |
**Real little-endian POWER10 hardware** (SpiderMonkey with debug, GCC 14, sponsored by Oregon State University):
| Suite | POWER10 default | `MOZ_PPC64_FORCE_POWER9=1` |
|---|---|---|
| Full jit-tests (`--jitflags=all`) | **13,715 / 0** | **13,715 / 0** |
| Full `jstests` (JSPI on) | **PASSED ALL** | **PASSED ALL** |
**ARM64-host simulator for ppc64le**:
| Suite | `FORCE_POWER8=1` | `FORCE_POWER9=1` | `FORCE_POWER10=1` |
|---|---|---|---|
| Full jit-tests | **13,715 / 0** | **13,715 / 0** | **13,715 / 0** |
| Full `jstests` (JSPI on) | **PASSED ALL** † | **PASSED ALL** † | **PASSED ALL** † |
† `shell/os.js` ("killed process should not have exitStatus") was a parallel-load flake on slower hosts — the test sent SIGINT to a `sleep 60` child and could lose the signal-delivery race under heavy concurrent jstests load, then waitpid blocked until the child exited normally and the assertion failed. Fixed 2026-05-08 by switching to SIGKILL (which is uncatchable, atomic, and works under any load). Patch in this PR.
`FORCE_POWER10=1` implies `FORCE_POWER9=1` (matches what real-P10 silicon advertises in `hwcap2` — both `ARCH_3_00` and `ARCH_3_1`). Six clean sweeps total — zero hard FAIL, zero TIMEOUT.
**Real big-endian POWER9 hardware** — ELFv1 (Debian ppc64) and ELFv2 (Arch POWER ppc64), SpiderMonkey debug shell:
| Host | Tier | Full jit-tests (`--jitflags=all`) | Full `jstests` |
|---|---|---|---|
| ELFv1 | default (P9) | **0 correctness failures** (47 timeouts ‡) | **0 unexpected / ~53,940 pass** |
| ELFv1 | `MOZ_PPC64_FORCE_POWER8=1` | **0 correctness failures** (46 timeouts ‡) | **0 unexpected / ~53,940 pass** |
| ELFv2 | default (P9) | **0 correctness failures** (5 timeouts ‡) | **0 unexpected / ~53,940 pass** |
| ELFv2 | `MOZ_PPC64_FORCE_POWER8=1` | **0 correctness failures** (7 timeouts ‡) | **0 unexpected / ~53,940 pass** |
‡ Same small set of legitimately slow tests under `--jitflags=all`'s heaviest combinations (e.g. `--no-blinterp --no-baseline --no-ion --more-compartments`, `gc/bug-2049401.js`) exceeding the harness's default timeout under shared-host CPU contention — both boxes are VMs sharing hardware with other work. The *identical* test/flag combination also times out on real little-endian POWER10, confirming host-load noise rather than an endianness or correctness issue; every affected test passes cleanly when re-run in isolation.
## Big-endian (ppc64be) support
The same backend runs big-endian, on both the **ELFv1** and **ELFv2** ABIs, across POWER8/9/10:
- **All execution tiers** — Baseline interpreter, Baseline JIT, Ion, and the native regexp JIT.
- **Full WebAssembly**, including SIMD and atomics. v128 values follow a canonical little-endian register convention; linear memory, globals, atomics, and the JS↔wasm boundary byte-reverse only where the spec observes byte order.
- **ABI handling gated on `_CALL_ELF`, not byte order** — big-endian ELFv2 shares the little-endian call paths (no function descriptors), while ELFv1 uses `{entry, TOC, env}` descriptors for the JIT / regexp / wasm entry trampolines and symbolic-address calls.
- Byte-order fixes for JIT entry stubs, bailout stack slots, wasm stack arguments/results, and `mozilla::Result` narrow storage. jit-test and wasm spec-harness cases that hard-coded little-endian byte order are made endian-neutral.
### Big-endian rendering (full browser)
Firefox on big-endian renders correctly — confirmed on real X11, pixel-verified (correct chrome, page background, link colors, text, and DevTools UI) — via a fourth layer of endian work beyond the JIT itself:
- **NSS GHASH hardware acceleration** — the vendored PPC64 `vpmsumd`-based GHASH (AES-GCM) is made endian-correct rather than stubbed out on big-endian, plus a companion fix for a real (both-endian, not just BE) `gcm.c`/`ghash-ppc.c` struct-layout mismatch that corrupted the hardware GHASH dispatch and crashed on the first real TLS 1.3 handshake.
- **libwebp VSX decode kernels** — the `WMERGEH`/`WMERGEL` widening-merge fix, same underlying issue class as the GHASH fix: GNU C vector element indexing is memory-order, so a widening merge lands in the opposite half on big-endian.
- **Skia + software WebRender** — big-endian used to flip Gecko's `OS_RGBA` surface-format synonym to a memory layout (`A8R8G8B8`) that Skia has no color type for, hitting `MOZ_DIAGNOSTIC_CRASH("A8R8G8B8 unsupported by Skia")` on the first paint. Fixed by keeping the whole pixel pipeline on one layout (Skia's little-endian value convention) and converting only at the two real boundaries: Skia's scalar blitters (fills, AA text, LCD subpixel text, glyph masks — via a `BE_CONVERT` macro, no-op on little-endian) and the software-WebRender→X11 handoff (dirty-region swizzle in `RenderCompositorSWGL`).
- **Known remaining gap**: `SkRasterPipeline`'s load/store stages (used for gradients and image sampling through Skia) are unchanged and still assume little-endian — content exercising that path may still show incorrect colors on big-endian. Not yet hit by the JS/DOM-heavy jit-test and jstests suites above, which is why it wasn't caught by that matrix; flagging here rather than leaving it silently uncovered.
## Scope
- **19 new files** in `js/src/jit/ppc64/` — ~17,500 lines.
- Modifications to ~60 shared files (dispatch headers, build system, wasm, CacheIR, MacroAssembler, WasmStacks).
- Full Firefox browser builds successfully (GCC + lld).
## What's new vs the old [chmeeedalf/gecko-dev](https://github.com/chmeeedalf/gecko-dev) Firefox-86 port
Inherited: `Architecture-ppc64.h` register/ABI constants, the `Assembler-ppc64.h/cpp` instruction-encoding skeleton, some basic MacroAssembler patterns.
New in this port:
- Build system integration (`js/moz.configure`, dispatch headers, `js/src/jit/moz.build`).
- **Full WebAssembly** — baseline + Ion, tiering, signal handlers, builtins, tail calls, atomics, huge memory, SIMD, JSPI. The old fork had zero Wasm.
- **Wasm SIMD** — complete VMX/VSX (~280 methods), all 10 tiers, full spec tests, both P8 and P9.
- **Warp/Ion backend** — complete `CodeGenerator`, Lowering visitors, LIR-ppc64 (old fork predated Warp).
- **Atomic operations** — ~980 lines of `CompareExchange` / `AtomicExchange` / `AtomicFetchOp`.
- **POWER8/9/10 runtime detection** via `getauxval(AT_HWCAP2)`, with fallback sequences for every POWER9-only instruction and POWER10 fast paths gated on `HasPOWER10()`.
- **Code-size optimizations** — inline constant pool for SIMD + FP constants, POWER9 `addpcis` FP-constant path, POWER10 prefix-instruction fast paths (see below), GCC-style `lis+ori+rldimi` 64-bit load (~37 % smaller than a `bcl`-based stanza).
- Long-branch relocation, OSI epilogue padding, QEMU-style `FlushICache`.
## Key architecture-specific features
### Runtime POWER8/POWER9/POWER10 selection
Detection via `getauxval(AT_HWCAP2)` — `PPC_FEATURE2_ARCH_3_00` for POWER9, `PPC_FEATURE2_ARCH_3_1` for POWER10.
- **POWER9 native paths** (with P8 fallbacks): `cnttzd/w`, `xxbrd`, `mtvsrws`, `mcrxrx`, `addpcis`, `modsw/u/d/du`, `vinsert{b,h}` (lane insert from VR + immediate byte position), `xxinsertw`, `mfvsrld`, `mtvsrdd`, `xs{min,max}jdp`, `xxspltib`, `vcmpne{b,h,w}`, `vneg{w,d}`, `isel`, `xscvdphp`/`xscvhpdp` (FP16 scalar conversion), `xsxexpdp` (P9 exponent extract for `nearbyIntDouble`). `replaceLaneInt{8x16,16x8}` on POWER9 emits the 2-insn `mtvsrd + vinsert{b,h}` sequence (down from 5 insns). `clampDoubleToUint8` is fully branchless on P9 via `xsmaxjdp + isel` — the canonical JS clamp shape collapses to a 4-insn straight-line sequence.
- **POWER10 fast paths** (gated on `HasPOWER10()`):
- **Prefix-instruction memory traffic — generic, not constants-only.**
- **Loads** (`Address` operands when displacement exceeds 16 bits): `loadPtr` → `pld`, `loadDouble` → `plfd`, `loadFloat32` → `plfs`, `loadUnalignedSimd128` → `plxv`. Replaces the prior `addis + ori + ldx`-style two-insn stanzas with one prefixed insn.
- **Stores** (same shape): `storePtr` → `pstd`, `storeDouble` → `pstfd`, `storeFloat32` → `pstfs`, `storeUnalignedSimd128` → `pstxv`.
- **Constant materialisation**: `loadConstantSimd128` 3 + `xxpermdi` → 1 `plxv`; `loadConstantDouble`/`Float32` 2 → 1 `plfd`/`plfs`; `movePtr(ImmWord)` 33–34-bit signed 5 → 1 `paddi`.
- **Single-instruction lane / byte-reverse / lane-insert collapses**: `brd` collapses `byteSwap64` 3 → 1 + drops the GPR↔FPR round-trip; `brh`/`brw` collapse `byteSwap16{Sign,Zero}Extend`/`byteSwap32` from 3-4 → 2; `vinsw`/`vinsd` collapse `replaceLaneInt{32x4,64x2}` from 2 → 1 (no scratch VSR); `vinsbrx`/`vinshrx` cover the same byte/halfword replaceLane on POWER10; `vextract{b,h,w,d}m` collapses bitmask to 1 insn; `vmulld` collapses `mulInt64x2` from 9-11 insns to 1.
- **CR-bit-to-GPR collapses via `setbc`/`setbcr`**: 1-insn `ma_cmp_set`, 1-insn `allTrueInt*` CR6.EQ extract, and 5 → 3 insn `anyTrueSimd128`. Replaces the older `mfocrf + rlwinm + andi.` extraction pattern across the boolean-reduction family.
Test with `MOZ_PPC64_FORCE_POWER8=1` (downgrade), `MOZ_PPC64_FORCE_POWER9=1` (force P9 on, useful under sim), or `MOZ_PPC64_FORCE_POWER10=1` (which implies `FORCE_POWER9=1` — matches real-P10 silicon detection). All three configurations PASS the full jit-test + jstests sweeps end-to-end on the simulator.
### POWER10 prefix-instruction support
Power ISA v3.1 prefixed instructions are 8 bytes (4-byte prefix word with primary opcode 1 + 4-byte suffix word) and must not straddle a 64-byte aligned block **at runtime**. The assembler emits them via `as_paddi` / `as_pld` / `as_plxv` / `as_plfd` / `as_plfs`, with an automatic leading-nop guard when the prefix would otherwise cross the boundary.
The original guard checked `(currentOffset() & 63) == 60` — a buffer-relative test that's only correct when the JitCode allocator base is 64-byte aligned. The allocator only guarantees 16-byte alignment, so across the four base-mod-64 classes {0, 16, 32, 48} the unsafe straddle is exactly `(currentOffset() & 15) == 12`. The same fix shape applies to the inline-emit path (`ensurePrefixedAlignment`, commit `8ae818d0fcab`) and the constant-pool patcher (`PatchConstantPoolLoad`, commit `aea233e21209`). The simulator's POWER10 model doesn't trap misaligned prefixes, which is why these only surfaced when real-P10 silicon came online.
The simulator decodes prefixed instructions, validated against `as -mpower10` + `objdump` on Fedora 44 / binutils 2.46. Encoding details (MLS Type=2 vs 8LS Type=0 vs 8RR Type=1, R-bit at LE position 20, `plxv` 5-bit suffix opcode + TX bit at suffix bit 26) live in `Simulator-ppc64.cpp` source comments and `PLAN.md`'s engineering-lessons section.
### WASM SIMD
Full VMX/VSX implementation using PPC64 AltiVec. All 10 tiers — load/store/splat/lane, bitwise, arithmetic, cmp/shift, float, conversions, shuffle/swizzle, extmul/pairwise/dot, relaxed SIMD (FMA/FNMA). NaN semantics correct (min/max propagation, truncSat NaN→zero, promote NaN-quieting). P8 uses `lxvd2x`/`stxvd2x` with byte-swap; P9 uses native `lxvx`/`stxvx`; P10 collapses the constant-pool variant to a single `plxv`.
### WASM JSPI
`WebAssembly.Suspending` / `WebAssembly.promising` via `javascript.options.wasm_js_promise_integration`. Add `ppc64` to the JSPI whitelists in `js/moz.configure` (both the `default_wasm_jspi` simulator allowlist and the `wasm_jspi` simulator-guard die-list — extending the original target.cpu enable to cover sim builds too), and round `ContRedZoneSize` up to a page-size multiple so the bottom guard page lands on a page boundary under PPC64LE's 64 KiB pages. No arch-specific stack-switch code required.
### WASM huge memory
4 GB + 32 MB virtual reservation per wasm memory, bounds-check elimination via SIGSEGV → wasm trap. Signal-handler context indices: `gp_regs[32]=NIA`, `gp_regs[1]=SP`, `gp_regs[31]=FP`, `gp_regs[36]=LR`.
## Simulator (ARM64-host)
A full PPC64 simulator is included so CI can run the JIT on non-PPC64 hosts. Both jit-test and jstests sweeps pass under `FORCE_POWER8=1`, `FORCE_POWER9=1`, and `FORCE_POWER10=1` configurations (zero hard FAIL, zero TIMEOUT). The sim received many ISA-correctness fixes during development (VMX/VSX decoding, LE lane ordering, FP NaN-payload preservation across f32↔f64, ELFv1 FP-arg shadow slots, single-step profiling for tail calls and non-leaf epilogues), and now decodes:
- **Power ISA v3.0 (POWER9)**: `addpcis`, `mods*/modu*`, `mtvsrws`, `xxspltib`, `xxinsertw`, `xscvdpsp(n)` HW-conformance fix, `xs{max,min}jdp`, `xscvdphp`/`xscvhpdp` (FP16), `xsxexpdp`. Closes the historical "~40 wasm/simd tests fail under FORCE_POWER9 on the sim" coverage gap.
- **Power ISA v3.1 (POWER10)**: prefixed instructions (`paddi`/`pld`/`plxv`/`plfd`/`plfs`), plus the new instruction-count-reduction ops (`brd`, `brh`, `brw`, `vinsw`, `vinsd`, `vinsbrx`, `vinshrx`, `vextract*m`, `setbc`/`setbcr`, `vmulld`).
- A simulator-side `setXEROV()` fix that mirrors OV→OV32 — the JIT's `branchMulPtr` Overflow path uses POWER9's `mcrxrx`+`bc` which reads OV32, and the original sim only set OV. Surfaced as silent BigInt fast-path-mul wrap-around in `wasm/regress/bug1836708.js`.
## Build
**SpiderMonkey (standalone JS shell):**
```
ac_add_options --enable-application=js
ac_add_options --enable-optimize
ac_add_options --disable-debug
ac_add_options --disable-tests
ac_add_options --enable-jit
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-sm-release
./mach build -j20
```
(On the PPC64 box, cap parallelism at `-j20`; the auto-detected `-j32` saturates the box during `lld` linking.)
**Full Firefox:**
```
export CC=gcc
export CXX=g++
ac_add_options --enable-application=browser
ac_add_options --enable-optimize
ac_add_options --disable-debug
ac_add_options --enable-release
ac_add_options --disable-tests
ac_add_options --enable-jit
ac_add_options --with-ccache
ac_add_options --with-libclang-path=/usr/lib64
ac_add_options --without-wasm-sandboxed-libraries
ac_add_options --enable-linker=lld
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-ff-ppc64
./mach build -j20
```
**Full Firefox with Clang + LTO (ppc64le):**
A Clang + ThinLTO build works on ppc64le and is measurably faster on
DOM/layout-heavy workloads than the plain GCC `-O2` build. Two things make it
work: **Clang with the LLVM linker (`lld`)**, and **system NSS** — the bundled
NSS `ppc-gcm.s` uses GAS-style semicolon statement separators that Clang's
integrated assembler rejects, so building against the distro's NSS
(`--with-system-nss`) sidesteps that entirely. This is exactly what the Fedora
package build does.
```
export CC=clang
export CXX=clang++
ac_add_options --enable-application=browser
ac_add_options --enable-optimize
ac_add_options --disable-debug
ac_add_options --enable-release
ac_add_options --disable-tests
ac_add_options --enable-jit
ac_add_options --enable-linker=lld
ac_add_options --enable-lto # ThinLTO
ac_add_options --with-system-nss # avoids Clang IAS choking on NSS ppc-gcm.s
ac_add_options --with-system-nspr
ac_add_options --with-libclang-path=`llvm-config --libdir`
ac_add_options --without-wasm-sandboxed-libraries
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-ff-clang
./mach build -j20
```
> **Do not add PGO on ppc64le.** Profile-guided optimization is currently
> broken on this target — GCC ICEs during the instrumented build and Clang's
> instrumented build crashes at runtime (see
> [DKnoto/Firefox-Benchmarks](https://github.com/DKnoto/Firefox-Benchmarks)).
> LTO alone is the supported configuration.
> `--with-libclang-path` must point at the directory that actually holds
> `libclang.so`. On distros that version the LLVM tree (e.g. Fedora's
> `/usr/lib64/llvm22/lib64`) let ``llvm-config --libdir`` resolve it rather than
> hard-coding `/usr/lib64`.
For RPM packagers: the Fedora spec builds this with
`rpmbuild --with build_with_clang --with build_with_lto`.
### Big-endian (ppc64be): Node.js bootstrap
The full browser build runs Node.js at build time to transform the DevTools
debugger modules (`devtools/client/*` via `mozbuild/action/node.py`). Nothing
Node produces ships in the binary — it is a build-time tool only — but the
build cannot finish without it, and Node on big-endian ppc64 is currently a
problem:
- Mozilla ships no prebuilt Node toolchain for ppc64, so the build needs a
system Node.
- Debian's ppc64 (BE) `nodejs` is an *unofficial port* frozen at 20.19.5 and
its `libnode` **segfaults** during a V8 conservative-stack-scan GC
(`heap::base::Stack::IteratePointersImpl`) when running the DevTools
transform — an endianness-specific V8/Node bug, unrelated to this port.
Two ways to get past it:
**1. Skip the DevTools client (simplest).** Set the DevTools build to
`server` instead of `all`. This drops the browser-side debugger UI but keeps
the DevTools *server* (remote debugging still works) and needs no Node:
```
export MOZ_DEVTOOLS=server # requires a one-line source edit; see note below
```
`MOZ_DEVTOOLS` cannot be set from `mozconfig` (it is an `imply_option`), so
change `imply_option("MOZ_DEVTOOLS", "all")` to `"server"` in
`browser/moz.configure`, or wait for a proper `--disable-devtools`-style knob.
**2. Offload Node to a working host (full DevTools).** If you have a
little-endian ppc64le box (or any box with a working Node), run only the Node
steps there while the compile stays on the BE machine. Node just reads/writes
files in the source and object trees, so share the tree and forward `node`:
- On the LE helper, mount the BE build tree (e.g. via `sshfs`) and confirm its
Node runs `devtools/client/shared/build/build.js` without crashing.
- On the BE box, put a `node` wrapper first on `PATH` that `ssh`es the command
to the helper, rewriting the tree-path prefix (BE mountpoint ↔ helper
mountpoint) in both the arguments and in the `dep:` lines Node prints back
(the build system consumes those for incremental rebuilds):
```sh
#!/bin/bash
# ~/nodewrap/node — forward Node to an LE helper that shares this tree.
LOCAL=/path/to/be/firefox # tree path on the BE box
REMOTE=/path/to/helper/mount # same tree, as seen on the helper
args=(); for a in "$@"; do args+=("${a//$LOCAL/$REMOTE}"); done
out=$(ssh helper "cd $(printf %q "${PWD//$LOCAL/$REMOTE}") && \
node $(printf '%q ' "${args[@]}")"); rc=$?
printf '%s\n' "${out//$REMOTE/$LOCAL}"; exit $rc
```
```
PATH=~/nodewrap:$PATH ./mach build -j20
```
Longer term the clean fix is a Node that does not crash on ppc64 BE (e.g.
building Node from source — Arch POWER's `nodejs-lts-iron` recipe builds
20.20.0 for `powerpc64` with `--with-intl=system-icu`).
## Known limitations
- **Clang full Firefox:** works with `--with-system-nss` + `lld` (see the Clang + LTO build recipe above). Only the *bundled* NSS is a problem — its `ppc-gcm.s` uses GAS-style semicolons that Clang's integrated assembler rejects — so building against the distro NSS avoids it. PGO is still unavailable on ppc64le (GCC ICE / Clang runtime crash); LTO-only.
- **`ld.bfd`:** does link `libxul.so` successfully on ppc64le, but needs substantially more RAM during the link than lld (community reports ~20+ GiB resident, putting it out of reach for hosts with under ~24 GiB total). The P10 box uses BFD as a workaround for an LLD `R_PPC64_TOC16_HA` bug; lld stays the practical default everywhere else for speed.
## TODO
- [ ] Wrap up and ask @classilla to review and help with upstreaming: https://bugzilla.mozilla.org/show_bug.cgi?id=1860412
- [ ] [Upstream SKIA with Google](https://github.com/runlevel5/skia/pull/1)
- [ ] [Upstream libwebp with Google](https://github.com/runlevel5/libwebp/pull/1)