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

@smmathews smmathews commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

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

R5900Context's constructor memsets the struct to zero and then re-seeds an 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 a default-constructed context starts at
(0,0,0,0), with w reading 0 instead of the hardware constant 1. Recompiled VU0
macro-mode math that uses vf0.w as the constant-1 homogeneous source — building a
translation or bias term, for example — then reads 0. There is no crash and no assertion,
only an arithmetic result off by whatever term depended on vf0.w being 1.

The runtime's primary context has a second problem. PS2Runtime::PS2Runtime()
default-constructs m_cpuContext, then memsets that same member back to zero and
re-seeds only r[0]. That memset clears a constructor pin, so the context that executes
recompiled VU0 macro-mode code stays wrong until a VU0 microprogram finishes and
copyVu0StateToContext repins vf0 after the fact.

Fix

  • ps2xRuntime/include/ps2_runtime.h — pin vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f)
    in the R5900Context constructor, next to the existing vu0_q seed. _mm_set_ps takes
    its arguments in (w,z,y,x) order, so this is x=0, y=0, z=0, w=1.
  • ps2xRuntime/src/lib/ps2_runtime.cpp — repeat the pin in PS2Runtime::PS2Runtime(),
    immediately after the r[0] re-seed that follows the memset of m_cpuContext. The
    memset drops the constructor's pin, so the repin is necessary for the runtime's own CPU
    context. It mirrors the r0 idiom on the line above it.

No signature, layout, or ABI change. No recompiler change.

Testing

Two tests in the PS2RuntimeExpansion suite, each asserting all four lanes of vf0:

  • R5900Context constructor pins vf0 to hardware (0,0,0,1) — a bare R5900Context ctx;,
    with no runtime and no microprogram. Its w assertion fails without the header pin. It
    never touches PS2Runtime, so it cannot catch the memset bypass.
  • PS2Runtime's own CPU context has vf0 pinned to hardware (0,0,0,1) — reads
    runtime.cpu().vu0_vf[0] on a freshly constructed PS2Runtime. This is the test that
    constrains the memset: on a tree with the header pin but without the ps2_runtime.cpp
    repin, its w assertion fails while the bare-context test still passes.
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

Risk and not in scope

  • Initialization only. This does not enforce the hardware discard-on-write rule for
    vf0's lifetime. The recompiler still emits plain writes to vector register index 0, so
    a later VU0 macro-mode write can clobber vf0 mid-function. Write-discard is a
    recompiler/codegen concern and stays out of scope. Read this as "vf0 now starts
    hardware-correct", not "vf0 is hardware-correct for the life of the context".
  • The pre-existing vf0 pin in copyVu0StateToContext is left untouched. It runs at the
    end of a VU0 microprogram, so it does not cover a context that has not run one.
  • Rebase: both touched files are also touched by other open PRs. Locate R5900Context and
    its constructor by name in the header, and locate the m_cpuContext memset and r[0]
    re-seed by name inside PS2Runtime::PS2Runtime(). Do not use line numbers.
Evidence: every runtime construction site of R5900Context, every assignment to vu0_vf[0], and every memset of a context — with commands

Construction sites in the runtime (test sources excluded):

$ git grep -nE 'R5900Context +[A-Za-z_]+ *(\{\}|;|=)' -- ps2xRuntime
ps2xRuntime/include/ps2_runtime.h:539:    R5900Context m_cpuContext;
ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp:688:            R5900Context callbackCtx{};
ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp:1080:            R5900Context temp = *ctx;
ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp:1152:            R5900Context temp = *ctx;
ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp:1340:            R5900Context callbackCtx = *callerCtx;
ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h:226:                    R5900Context callbackCtx{};
ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h:372:    R5900Context tmp = *ctx;
ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp:162:                R5900Context irqCtx{};
ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp:267:                R5900Context irqCtx{};
ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp:358:            R5900Context threadCtxCopy{};
ps2xRuntime/src/lib/ps2_iop_host.cpp:433:    R5900Context context{};

The {} and bare-declaration rows run the constructor, so the header pin covers them
(callback, interrupt, thread, and IOP-host contexts). The four = *ctx rows are copies
and take vf0 from their source, which is the correct copy behaviour.

Assignments to vu0_vf[0] in the runtime, after this change:

$ git grep -n 'vu0_vf\[0\] *=' -- ps2xRuntime
ps2xRuntime/include/ps2_runtime.h:155:        vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f);
ps2xRuntime/src/lib/ps2_runtime.cpp:271:        ctx->vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f);
ps2xRuntime/src/lib/ps2_runtime.cpp:529:    m_cpuContext.vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f);

Line 271 is the pre-existing pin inside copyVu0StateToContext, which
PS2Runtime::executeVU0Microprogram calls after m_vu0.execute returns. Lines 155 and
529 are this change.

Memsets that could clear a pinned context:

$ git grep -nE 'memset\(&?(m_cpuContext|ctx|context|[a-zA-Z_]*[Cc]tx)' -- ps2xRuntime
ps2xRuntime/src/lib/ps2_gs_gpu.cpp:445:    std::memset(m_ctx, 0, sizeof(m_ctx));
ps2xRuntime/src/lib/ps2_runtime.cpp:522:    std::memset(&m_cpuContext, 0, sizeof(m_cpuContext));

m_ctx in ps2_gs_gpu.cpp is the GS drawing-context array, not an R5900Context. The
m_cpuContext memset is the one the second pin answers.

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