Skip to content

fix(dma): transfer the VIF chain tag half for every tag id - #190

Draft
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/28-vif1-chain-tag-transfer
Draft

fix(dma): transfer the VIF chain tag half for every tag id#190
smmathews wants to merge 1 commit into
ran-j:mainfrom
smmathews:feature/28-vif1-chain-tag-transfer

Conversation

@smmathews

@smmathews smmathews commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

The DMA chain walker in PS2Memory::writeIORegister appended a chain tag's upper
half — the 8 bytes at offset +8 within the tag — to the VIF stream only for tag ids 1, 2, 5, 6 and 7
(CNT/NEXT/CALL/RET/END). Ids 0, 3 and 4 (REFE/REF/REFS) were skipped.

REF/REFS/REFE are the ids whose half is most likely to carry something, because their
payload lives elsewhere: a builder can put an STCYCL+UNPACK pair or a DIRECT header in
the tag half and point the tag at a separate block of raw data. Dropping it
means the vifcode that would have consumed the referenced block never runs, the
referenced bytes decode as vifcode instead, and the stream desyncs from there.

Fix

  • ps2_memory.cpp — append the tag half once per tag, after the id switch, for both
    VIF channels (0x10008000 and 0x10009000). Payload sourcing is unchanged: for ids
    1/2/5/6/7 the removed special case read from the tag's own +16 offset, which is what
    dataAddr already holds for them.
  • ps2_memory.cpp — raise the walk cap from 4096 to 65536 tags and print one
    diagnostic per process when it is hit. The cap guards against a self-referential NEXT
    tag hanging the walker; at 4096 a long but legitimate chain was truncated with no
    indication. The cap stays, because removing it would trade truncation for a hang.
  • ps2_vif1_interpreter.cpp — move the PATH2 image continuation from the top of the
    vifcode loop into the DIRECT/DIRECTHL handler. The old loop drained the next
    16-byte-aligned bytes as image pixels whenever a continuation was pending, without
    checking whether they were a vifcode; once REF/REFS/REFE halves are transferred, a
    real vifcode (NOP padding) can sit between the opening DIRECT and the continuing one.
    A continuation is now satisfied from the front of a later DIRECT/DIRECTHL payload, the
    rest of that payload is that DIRECT's own content, and any other vifcode runs
    normally.

Hardware basis

  • A DMAtag's upper 64 bits sit at a fixed offset for all eight tag ids; the id selects
    the payload and next-tag addresses, not whether that field exists.
  • With CHCR.TTE set the channel receives that half; on a VIF channel it enters the
    stream as two 32-bit vifcodes.

Testing

New tests in ps2xTest/src/ps2_memory_tests.cpp drive PS2Memory and
processVIF1Data directly:

  • A REF tag's upper-half UNPACK executes, and the quadword it references lands at the
    expected VU1 offset with the expected bytes.
  • An MSCAL in a later zero-QWC tag half fires once with startPC 800, following a REF tag
    whose own half is non-empty.
  • A NOP between an image-mode DIRECT and its continuation DIRECT executes as a vifcode,
    and the continuation's image quadwords come from that second DIRECT's own payload.
  • A 4200-tag chain reaches its real END tag and that tag's ITOP takes effect.

One existing test continued a PATH2 image upload with unwrapped quadwords; it now
continues it with a follow-up DIRECT.

./build/ps2xTest/ps2x_tests

Risk / not in scope

  • Appending unconditionally is a superset of the previous behaviour on the VIF channels:
    it adds bytes that were dropped before and removes none.
  • Remaining risk: a producer running with TTE clear that stores meaningful non-zero
    scratch in the tag half. Unused padding decodes as a pair of NOPs.
  • TTE gating is not in scope. The latched CHCR value at walk time does not reliably
    reflect TTE here, so gating on it today would drop the half for chains that currently
    render correctly. How this runtime merges CHCR writes is a separate, unstarted
    investigation, and unconditional transfer composes with a later TTE-gated narrowing.
  • feat(runtime): deferred INTC pending-raise latch with per-tick drain #151 (INTC pending-latch drain) touches both files — a store and a new consumer method
    in ps2_memory.cpp, two lines in an IRQ block in ps2_vif1_interpreter.cpp. Neither
    hunk overlaps a region changed here.
Fail-before signatures and a source-chain census

Each new test was written and run against the unmodified tree first, and failed there
with the signature below.

Test Failure before the fix
REF tag half UNPACK The referenced quadword is decoded as vifcode rather than consumed as UNPACK data, so nothing lands at the expected VU1 offset.
MSCAL after a REF tag Dropping the REF tag's half desyncs the parser far enough that the MSCAL is never reached.
DIRECT continuation The raw drain eats the NOP as image pixels, and the continuation quadwords are read from the wrong offset.
4200-tag chain The chain stops at the 4096-tag cap and the final tag's ITOP never takes effect.

Background, not part of the automated gate and not reproducible from this branch: a VIF1
source chain captured from a frame was walked and its decoded vifcode stream censused
three ways — current main behaviour, unconditional append, and TTE-gated append. Under
the unconditional rule the stream decoded without unrecognized opcodes and without
out-of-range microprogram kicks; the other two rules each produced some. The tests
above, not this census, are what the suite enforces.

The DMA chain walker only appended a chain tag's upper-half vifcodes to
the VIF stream for CNT/NEXT/CALL/RET/END tags, skipping REFE/REF/REFS
entirely. That gate conflates the tag id, which says where a tag's
payload lives, with the tag's own upper quadword, which hardware
transfers for every tag id when CHCR.TTE is set. REF/REFS/REFE are
exactly the tags most likely to put something meaningful in that half,
since their payload lives elsewhere; dropping it desyncs the parser
because the referenced payload gets decoded as vifcode instead of being
consumed as the data it is.

Make the append unconditional per tag on both VIF channels instead of
gating it on id, since the latched CHCR does not reliably expose TTE
here and gating on it would regress chains that work today.

Fixing that alone regresses PATH2 image continuation, because the
interpreter used to drain pending continuation data as a raw byte grab
at the top of its loop, with no regard for whether those bytes were
actually a vifcode. Now that a chain can legitimately contain a real
vifcode between an image DIRECT and its continuation, move the drain
into the DIRECT/DIRECTHL handler itself: a pending continuation is only
ever satisfied by a follow-up DIRECT/DIRECTHL, out of the front of that
vifcode's own payload.

Also raise the chain-tag walk cap, which was low enough to silently
truncate a legitimately long chain, and add a one-time diagnostic for
when it is hit so a truncation is visible instead of silent. The cap
itself stays, as the only guard against a self-referential NEXT tag
hanging the walker.

Four tests pin all three pieces directly against processVIF1Data and
PS2Memory, each written and confirmed failing against the unmodified
tree before the fix, with no game content involved.
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