Skip to content

ppc64-QVM: data-check refactor + immediate signedness fixes - #398

Merged
ec- merged 3 commits into
ec-:mainfrom
runlevel5:refactoring
May 19, 2026
Merged

ppc64-QVM: data-check refactor + immediate signedness fixes#398
ec- merged 3 commits into
ec-:mainfrom
runlevel5:refactoring

Conversation

@runlevel5

@runlevel5 runlevel5 commented May 17, 2026

Copy link
Copy Markdown
Contributor

a8b2080d — drop branch-trap data check, always mask in emit_CheckReg

The cmplw+bgt+trap path in emit_CheckReg is no longer worth
its overhead once the function also has to neutralize 64-bit ADD
overflow on the address register: the predictability that made
the check cheap is gone the moment we touch the register. Switch
to the data-mask path unconditionally — same wrap semantics as
the interpreter, one AND instruction, no branch, no trap stub,
smaller i-cache footprint.

Drops the now-dead forceDataMask global (declared but never
assigned, so the PPC64 JIT never actually took the existing AND
short-circuit), the FUNC_BADR/FUNC_BADW enum values and their
trampoline entries, and the ErrBadDataRead/ErrBadDataWrite
helpers.

05329ea2 — tighten CONST_OPTIMIZE fit check for BAND/BOR/BXOR

The shared (int16_t)v == v fit check let CONST X; BAND/BOR/BXOR
fold for X in [-32768, -1] (e.g. CONST -4; BAND for 4-byte
pointer alignment). PPC's ANDI/ORI/XORI use an unsigned
16-bit immediate zero-extended to 32 bits, so the fold produced
ra & 0x0000FFFC instead of ra & 0xFFFFFFFC, clobbering the
upper 16 bits of the operand. When ra was a pointer into the
hunk-allocated data segment (offset > 65535), the masked result
was garbage, and later linked-list traversal walked into a
self-referential node — observed as the q3ut4 UI VM infinite
spin during shader registration (fixes #397).

Split the immediate-fit check: ADD/SUB/MULI/MULU keep the
signed check (matching ADDI/MULLI semantics, with an added
guard against ADDI(-imm) overflow when ci->value == INT16_MIN
for SUB); BAND/BOR/BXOR now require the upper 16 bits of
ci->value to be zero before folding.

0c6e7dfb — debug-only emit-macro range checks + diagnostic data trap

Two debug-only additions (compiled out in release) and one
correctness fix in the same vein as the BAND/BOR/BXOR fix above.

  • D-form immediate range checks. PPC_D silently truncates
    its immediate to 16 bits, so a caller passing a wider value
    gets a wrong instruction with no warning. Wrap the macros
    in PPC_IMM_S16 (ADDI, ADDIS, SUBFIC, MULLI, LI,
    LIS, CMPWI) and PPC_IMM_U16 (ANDI, ORI, ORIS,
    XORI, XORIS, CMPLWI). On out-of-range, Com_Error at
    JIT-compile time naming the opcode. Useful as defence against
    the same signedness-confusion bug class.

  • Diagnostic data-segment trap. emit_CheckReg now takes
    (vm, qvm_ip) and, in debug builds, emits a CMPLD against
    rDATAMASK and calls a FUNC_HIBITSET / ErrHighBitsSet(qvm_ip, bad_addr) stub on overflow. Release builds emit only the
    unconditional AND mask, same as a8b2080d.

  • CONST_OPTIMIZE OP_LTU/LEU/GTU/GEU fit check. The
    prior shared (int16_t)v == v check let negative ci->value
    fold into PPC_CMPLWI, whose immediate is zero-extended — so
    the JIT silently compared against the 16-bit-truncated value.
    Split the fit check by signedness; same root cause as the
    BAND/BOR/BXOR case above.

The cmplw+bgt+trap path in emit_CheckReg is no longer worth its overhead
once we also have to neutralize 64-bit ADD overflow on the address register:
the "address is unmodified, branch always-not-taken" predictability that
made the check cheap is gone the moment we touch the register. Use
the data-mask path unconditionally - same wrap semantics as the
interpreter, one AND instruction, no branch, no trap stub, smaller
i-cache footprint.

While here, drop dead code surfaced by this simplification:

- The static `forceDataMask` global was declared but never assigned,
  so the PPC64 JIT never actually took the existing AND short-circuit;
  the cmplw/bgt path was the only one in effect. Remove the global
  and the three caller-side conditionals that gated on it (which
  unconditionally produced R4|RCONST, an unsafe hint now that AND
  modifies the address in place).
- FUNC_BADR/FUNC_BADW enum values, their long-branch entries in the
  per-VM dispatch prologue, the funcOffset/emitFuncEntry stubs at
  end-of-codegen, and the ErrBadDataRead/ErrBadDataWrite C helpers
  are now unreachable - remove them.
- Adjust FUNC_ERR_END to point at FUNC_OUTJ (last remaining error
  function).
The shared signed-16-bit check let CONST X; BAND/BOR/BXOR fold for X in
[-32768, -1] (e.g. CONST -4; BAND for 4-byte pointer alignment).  PPC's
ANDI/ORI/XORI use an unsigned 16-bit immediate zero-extended to 32 bits,
so the fold produced ra & 0x0000FFFC instead of ra & 0xFFFFFFFC,
clobbering the upper 16 bits of the operand.  When ra was a pointer into
the hunk-allocated data segment (offset > 65535), the masked result was
garbage, and later linked-list traversal walked into a self-referential
node — observed as the q3ut4 UI VM infinite spin during shader
registration.

Split the immediate-fit check: ADD/SUB/MULI/MULU keep the signed check
(matching ADDI/MULLI semantics, with an added guard against ADDI(-imm)
overflow when ci->value == INT16_MIN for SUB); BAND/BOR/BXOR now require
the upper 16 bits of ci->value to be zero before folding.
@ec-

ec- commented May 18, 2026

Copy link
Copy Markdown
Owner

The JIT keeps opstack slots as TYPE_CONST/TYPE_LOCAL compile-time
metadata when possible — no register write, no memory write — and
materialises them lazily. At branch sources, this leaves the
branch-taken path without the values that the linear fall-through
path would eventually have written, so the target reads stale or
zero memory

Is this a real case from an existing code?

You are not supposed to do a jump on targets with different opStack assuming you will never resolve anything below its current value in branches between. Closest example is a ternary conditition (not properly implemented in LCC though): x = a ? b : c -- meaning everything after ? should not go below of an opStack of a

Two debug-only additions (compiled out in release) and one correctness
fix in the same vein as the BAND/BOR/BXOR fix in 05329ea.

Range checks for D-form immediate fields: PPC_D silently truncates
its immediate to 16 bits, so a caller passing a wider value gets a
wrong instruction with no warning.  Wrap the macros in PPC_IMM_S16
(ADDI/ADDIS/SUBFIC/MULLI/CMPWI; accepts the unsigned 16-bit bit
pattern too, since callers like emit_MOVi64 pre-extract uint16
halves) and PPC_IMM_U16 (ANDI/ORI/ORIS/XORI/XORIS/CMPLWI; rejects
upper-16-bits non-zero).  On out-of-range, fires Com_Error at
JIT-compile time naming the opcode.

Diagnostic data-segment trap: emit_CheckReg now also takes (vm,
qvm_ip) and, in debug builds, emits a CMPLD against rDATAMASK and
calls FUNC_HIBITSET / ErrHighBitsSet(qvm_ip, bad_addr) on overflow.
Release builds emit only the unconditional AND mask, same as before
a8b2080.

CONST_OPTIMIZE OP_LTU/LEU/GTU/GEU fit check: the prior shared
(int16_t)v == v check let negative ci->value fold into PPC_CMPLWI,
whose immediate is zero-extended -- so the JIT silently compared
against the 16-bit-truncated value instead of the original.  Split
the fit check by signedness; same root cause as the BAND/BOR/BXOR
case already fixed in 05329ea.
@runlevel5 runlevel5 changed the title ppc64-QVM: drop branch-trap data check, always mask in emit_CheckReg ppc64-QVM: data-check refactor + immediate signedness fixes May 19, 2026
@runlevel5

Copy link
Copy Markdown
Contributor Author

Is this a real case from an existing code?

Yes, from the stress test.

I have removed that changes from this PR. Once this PR is reviewed, I will open a new one then we could have further discussion

@ec-
ec- merged commit 608e44b into ec-:main May 19, 2026
28 checks passed
@runlevel5
runlevel5 deleted the refactoring branch May 19, 2026 05:21
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.

PPC64 VM: Client get stuck when launching Urban Terror

2 participants