Skip to content

Stop DMA chain walks on real loops instead of a 4096-tag cap - #417

Open
0xLeathery wants to merge 2 commits into
ran-j:mainfrom
0xLeathery:feat/dma-chain-loop-detection
Open

0xLeathery wants to merge 2 commits into
ran-j:mainfrom
0xLeathery:feat/dma-chain-loop-detection

Conversation

@0xLeathery

Copy link
Copy Markdown

Problem

The chain-mode DMA walk in ps2_memory.cpp stopped after kMaxChainTags = 4096 tags, silently, with no log. The remaining tags were never transferred. The real DMAC has no tag limit, and large 3D frames go past 4096.

Sly 2 (SCUS-97316) builds its first in-game frame as a VIF1 chain of 5,132 tags, mostly CALL/RET subroutine pairs carrying about 4.8 MB. Its tail was dropped, including the VU1 microprograms that draw (XGKICK) and the work that ends in the GS FINISH. The render thread then waited on FINISH forever.

Change

The cap only needed to stop chains that never end. Guest memory doesn't change while one chain is walked, so a chain loops forever exactly when it reaches a tag it has already processed with the same CALL/RET return state. The walk now detects that directly.

  • Loop detection. It stops when a NEXT, CALL or RET tag is revisited with the same tag address, ASP, ASR0 (when ASP ≥ 1) and ASR1 (when ASP = 2). The non-live ASRs are left out because a CALL overwrites them before any RET reads them. A repeated state is a real cycle, so a chain that ends on hardware is never stopped by mistake. The repeated tag isn't executed, so no payload is sent twice.
  • Safety cap. 1<<20 tags or 64 MB of transferred bytes, as a backstop for chains that never repeat a state.
  • Logging. Either guard logs [DMA] <channel> chain stopped at tag 0x… : <reason> after N tags, M bytes through RUNTIME_ERROR, because RUNTIME_LOG compiles out in Release. That's limited to 8 warnings per channel.
  • Unchanged: chains that end normally (end, refe, RET at ASP 0, TIE+IRQ), and the TADR/ASR/CHCR write-back after the walk.

Testing

New tests in ps2_memory_tests.cpp, all of which failed before the change:

  • A chain of about 5000 CNT tags arrives in full, through its END payload.
  • A chain of more than 4096 CALL/RET tags with two levels of nesting arrives in full. This catches a loop key that uses only the top return address.
  • A NEXT self-loop and a CALL/RET cycle each send their data once, then stop with a warning.

Results:

  • ps2x_tests passes 450/450.
  • Probes, not committed:
    • A subroutine containing a NEXT, called from two sites, isn't stopped.
    • 100,000 calls from distinct sites and a REF reused 10,000 times both arrive in full.
    • A recursive CALL stops.
    • A 12,001-tag VIF1 chain with TTE delivers every tag's upper half.
    • Timing in Release: about 1 ms for 15k CALL/RET tags, 24 ms for 300k.
  • With a recompiled Sly 2 (USA), the 5,132-tag frame now goes through without either guard firing. VU1 XGKICKs start and the render thread no longer hangs on FINISH.

Known limits, not changed here

  • This predates the change: a chain that stops with nothing transferred, such as a qwc=0 NEXT self-loop, never reaches the completion path, so no D_STAT bit is raised.
  • std::set keeps the code simple. unordered_set would make CALL/RET-heavy walks about 5x faster if that ever matters.

Built and tested on macOS arm64 (Apple clang).

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