Skip to content

Check that instruction immediates fit their encoding; Add warnings-as-errors option. - #3

Merged
fjtrujy merged 2 commits into
ps2dev:masterfrom
glampert:fix/immediate-ranges
Oct 20, 2026
Merged

fjtrujy merged 2 commits into
ps2dev:masterfrom
glampert:fix/immediate-ranges

Conversation

@glampert

Copy link
Copy Markdown

Checking that instruction immediates fit their encoding:

dvp-as keeps only the low bits of an immediate that doesn't fit its field, and truncates without errors or warnings. iaddi VI03, VI10, -18 assembles to the same word as +14, because IADDI's immediate is 5 bits, signed.

This issue was found on the Quake2 PS2 port project where a check computed iVertsLeft + 14 this way. openvcl passed such values through from the source. Its scheduler could also create out-of-range offsets itself when it moved a load or store across a pointer update.

What SCE's vcl 1.4beta7 (distributed with the Tyra engine) does, from this CI run:

Field Example vcl 1.4beta7
iaddi, 5 bits signed -18, 16 Emits every iaddi as isubiu/iaddiu, e.g. isubiu VI01,VI00,18
iaddiu/isubiu, 15 bits unsigned 40000, -1, 32768 Warns, then emits the value anyway
fcand/fcor/fcset, 24 bits 0x1000000 Emits it with no diagnostic
fsand/fsset, 12 bits 0x1000, 4096 Crashes on the fsand, deletes the fsset
lq/sq/ilw/isw offset, 11 bits signed 1024, -1025, 2000 Emits it with no diagnostic

With this PR:

  • An iaddi outside -16..15 is emitted as isubiu/iaddiu with the value's magnitude, as SCE's vcl does. An iaddi within -16..15 is unchanged. Outside ±32767 it's an error.

  • An iaddiu/isubiu, fc* or fs* immediate that doesn't fit is an error, for example x.vcl(10) : Immediate 40000 is outside 0..32767 (iaddiu iC, iA, >>> 40000 <<<).

  • A memory offset outside -1024..1023 is a warning, for example warning: Offset 2000 is outside -1024..1023 and assembles as -48. vcl accepts these, and VU1 addresses wrap at 1024 quadwords, so the truncated offset reaches the same address.

  • The value checked is the one openvcl emits: its own evaluation of the expression, or the integer literal dvp-as reads (hex, binary, octal). Symbols are left to the assembler. A line with a bad immediate is still compiled, so later lines don't get follow-on errors.

  • The scheduler no longer pushes an offset out of the field when it:

    • moves a store into a branch delay slot;
    • advances a pointer update above its stores (the same check as on the optimizations branch);
    • shifts software-pipeline prefetch and drained-store offsets (an unencodable next-iteration offset now counts as unknown);
    • builds multi-Q cyclic prefixes.
  • Merging two iaddiu now stays within 0..32767.

  • The second commit adds --warnings-as-errors, which makes any warning fail the run. We now emit a warning for the memory offset range check.

  • test/repro/quad_adc_bug.vcl used iaddiu vi05, vi00, 0xffff. That assembles as 0x7fff and would now be an error, so it now says 0x7fff.

Testing:

  • New test/integration/test_immediate_ranges.cpp with 14 cases.
  • The scheduler cases put each rewrite's offset at the edge of the field. Master emits e.g. lq VF01, 1024(VI01). With this PR every offset fits, and the output is byte-identical to master's wherever master's already fitted.
  • The full suite passes, also under ASan/UBSan. 37 VU programs (the ps2gl fixtures, the examples, Quake II and Tyra) compile byte-identically to master.

🤖 Co-authored by Claude Code

glampert and others added 2 commits October 19, 2026 22:53
dvp-as keeps only the low bits of an immediate that doesn't fit the
instruction's field, without a diagnostic: `iaddi VI03, VI10, -18`
assembles to the same word as `+14`, since IADDI's immediate is 5 bits,
signed. openvcl passed such values through from the source, and its
scheduler could create out-of-range offsets of its own when it shifted a
load or store across a pointer update.

Each instruction's immediate is now checked when it is parsed, using the
value openvcl emits (its own evaluation of the expression, or the integer
literal dvp-as reads):

- iaddi outside -16..15 is emitted as isubiu/iaddiu of the magnitude, the
  way SCE vcl 1.4beta7 emits every iaddi (`iaddi a, b, -18` becomes
  `isubiu a, b, 18`). Outside -32767..32767 it is an error.
- iaddiu/isubiu outside 0..32767, fcand/fceq/fcor/fcset beyond 24 bits
  and fsand/fseq/fsor/fsset beyond 12 bits are errors. vcl 1.4beta7 only
  warns on iaddiu/isubiu and passes the fc* masks through, and dvp-as then
  truncates them.
- lq/sq/ilw/isw offsets outside -1024..1023 are a warning. vcl 1.4beta7
  accepts them, and since VU1 addresses wrap at 1024 quadwords the
  truncated offset reaches the same address.

The scheduler no longer makes a change that would push an offset out of
the field: moving a store into a branch delay slot, advancing a pointer
update above its stores, software-pipeline prefetches and drained stores
(treated like an unknown next-iteration offset), and multi-Q cyclic
prefixes. Merging two iaddiu now keeps to IADDIU's unsigned range.

test/repro/quad_adc_bug.vcl used `iaddiu vi05, vi00, 0xffff`, which
assembles as 0x7fff; it now says 0x7fff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With it, any warning fails the run with a non-zero exit status, as an
error does. The first warning openvcl reports is the out-of-range memory
offset added in the previous commit, which a build can now choose to stop
on. The warning text is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fjtrujy fjtrujy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!
Good improvements.
Cheers!

@fjtrujy
fjtrujy merged commit 46ccc30 into ps2dev:master Oct 20, 2026
5 checks passed
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.

2 participants