Skip to content

fix(vu0): pin vf0 to (0,0,0,1) in every constructed R5900Context - #187

Draft
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/24-vf0-pin-context
Draft

fix(vu0): pin vf0 to (0,0,0,1) in every constructed R5900Context#187
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/24-vf0-pin-context

Conversation

@smmathews

Copy link
Copy Markdown
Contributor

fix(vu0): pin vf0 to (0,0,0,1) in every constructed R5900Context

The gap

On real VU0 hardware, vf0 is hardwired to (x,y,z,w) = (0,0,0,1) and always
reads back that constant, the vector-unit analogue of GPR r0.

R5900Context's constructor memsets the whole struct to zero and then
re-seeds a short explicit list of fields (vu0_q, cop0_random, cop0_status,
cop0_prid, in_delay_slot, branch_pc). vu0_vf[0] is not on that list, so
every default-constructed R5900Context starts at (0,0,0,0), with w read
as 0 instead of the hardware constant 1.

Nothing pins vf0 at construction today. The only existing vf0 pin in the
tree lives inside the VU0 state restore helper that runs at the end of a
VU0 microprogram execution; it repins vf0 after the fact but does not help
a context that has not yet run a VU0 microprogram. Several sites across
the runtime and kernel default-construct an R5900Context (the runtime's
own CPU context, plus assorted callback, interrupt, and thread contexts),
and each one runs the same unpinned constructor and therefore starts with
a wrong vf0.

The runtime's own CPU context needs an extra fix beyond the constructor
pin. PS2Runtime's top-level constructor default-constructs its embedded
m_cpuContext (running the constructor pin once), then immediately
memsets that same member back to zero and re-seeds only GPR r0. That
memset silently clears the pin a second time, so the primary context that
actually executes recompiled VU0 macro-mode code starts with vf0.w == 0
again, and stays wrong until the first VU0 microprogram runs the restore
helper.

Why it matters

Recompiled VU0 macro-mode math that treats vf0 as the constant-1
homogeneous source (for example building a translation or bias term from
vf0.w) reads 0 instead of 1 until something else happens to repin vf0
first. The wrong result is silent: no crash, no assertion, just an
arithmetic answer that is off by whatever term depended on vf0.w being 1.

The fix

Two parts:

  1. Pin vf0 to (0,0,0,1) directly in R5900Context's constructor, right
    after the existing vu0_q seed, so every default-constructed context
    (callback, interrupt, and thread contexts included) starts
    hardware-correct from the moment it exists.
  2. Repin vf0 a second time in PS2Runtime's own constructor, right after
    the existing r0 re-seed that follows its memset of m_cpuContext.
    That memset clears the pin the constructor just applied, so without
    this second repin the runtime's primary CPU context, the one that
    executes recompiled VU0 macro-mode code, would still start wrong. The
    repin mirrors the r0 idiom sitting immediately above it.

Caveat: initialization only

This pins vf0 at construction time. It does not enforce the hardware
discard-on-write invariant for vf0's lifetime: the recompiler still emits
plain writes to vector register index 0 like any other register, so a
later VU0 macro-mode write can still clobber vf0 mid function. That
write-discard behavior is a recompiler/codegen concern and is explicitly
out of scope here. Read this change as "vf0 now starts hardware-correct,"
not "vf0 is now hardware-correct for the life of the context."

Scope

  • Two functional lines across two files (the R5900Context constructor
    pin in ps2_runtime.h, plus the m_cpuContext repin in
    ps2_runtime.cpp), plus two new regression tests.
  • No signature, layout, or ABI change.
  • No recompiler change.
  • The existing vf0 pin inside the VU0 state restore helper is left
    untouched; it is not redundant with this change, since nothing else
    pinned a freshly constructed context before now.

Test

Two regression tests:

  • A direct-construction test asserting all four lanes of vf0 on a bare
    R5900Context ctx;, independent of executing any VU0 microprogram or
    constructing a runtime. On the unmodified constructor, the w assertion
    fails (reads 0 instead of 1); after this change it passes. This test
    guards the constructor pin only; it does not exercise PS2Runtime at
    all, so it cannot catch the memset bypass.
  • A constructed-runtime test asserting all four lanes of
    runtime.cpu().vu0_vf[0] on a freshly constructed PS2Runtime. This is
    the test that actually constrains the memset bypass: on a tree with the
    constructor pin but without the ps2_runtime.cpp repin, its w assertion
    fails while the bare-context test still passes, which is exactly the
    bypass this PR closes.

Repro:

cmake -S . -B build -DCMAKE_CXX_FLAGS="-msse4.1 -include cstdint" -DCMAKE_C_FLAGS=-msse4.1
cmake --build build --target ps2x_tests
./build/ps2xTest/ps2x_tests

Look for "R5900Context constructor pins vf0 to hardware (0,0,0,1)" and
"PS2Runtime's own CPU context has vf0 pinned to hardware (0,0,0,1)" in the
PS2RuntimeExpansion suite.

Rebase note

Both ps2_runtime.h and ps2_runtime.cpp are touched by other in-flight
work. Locate R5900Context and its constructor by name in the header,
and locate the m_cpuContext memset / r0 re-seed by name in
PS2Runtime::PS2Runtime(), not by line number in either file.

On real VU0 hardware vf0 is hardwired to (x,y,z,w) = (0,0,0,1), the
vector-unit analogue of GPR r0. R5900Context's constructor memset the
struct to zero and re-seeded only a short list of fields, leaving vf0
at (0,0,0,0) with w = 0. Pin vf0 in the constructor so every
default-constructed context, including the callback, interrupt, and
thread contexts built across the kernel, starts hardware-correct.

The runtime's own top-level constructor needed a second fix. It
default-constructs its embedded m_cpuContext, which runs the new
constructor pin, then immediately memsets that same member back to
zero and re-seeds only r0. That memset silently cleared the pin a
second time, so the primary context that actually executes recompiled
VU0 macro-mode code still started with vf0.w == 0, and stayed wrong
until the first VU0 microprogram ran the state restore helper. Repin
vf0 there too, right after the existing r0 re-seed, mirroring how r0
is already treated at that site.

Adds two regression tests: one on a bare R5900Context asserting all
four vf0 lanes, independent of any runtime, and one on a freshly
constructed PS2Runtime asserting the same four lanes on its own
m_cpuContext. The second test is the one that actually constrains the
memset bypass; deleting the repin line makes only that test's w
assertion fail while the bare-context test keeps passing, proving the
two tests guard different sites.
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