Skip to content

feat(ps1): Phase 2 GPU/GTE capture hooks (#418, #419)#581

Merged
fernandotonon merged 5 commits into
masterfrom
feat/ps1-gpu-gte-capture
May 18, 2026
Merged

feat(ps1): Phase 2 GPU/GTE capture hooks (#418, #419)#581
fernandotonon merged 5 commits into
masterfrom
feat/ps1-gpu-gte-capture

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 17, 2026

Summary

Test plan

  • UnitTests --gtest_filter="GpuCommandParser*:CaptureBuffer*:GteCapture*:PS1RipManager*" (Linux, offscreen)
  • CI unit-tests-linux with ENABLE_PS1_RIP=ON

Closes #418, #419.

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced PS1 geometry capture pipeline for runtime frame analysis
    • Added GPU command parser for decoding PS1 primitive graphics commands
    • Implemented matrix deduplication using hash-based caching to optimize storage
    • Captured frame data exportable as CSV for verification and debugging
  • Tests

    • Added comprehensive test coverage for capture infrastructure, buffer operations, GPU command parsing, and matrix operations

Review Change Stack

Add CaptureBuffer, RipperHooks, GpuCommandParser, and GTE matrix dedupe.
Wire capture arming to the worker thread; stub core emits sample primitives
when armed. Unit tests cover GP0 decoding, matrix hashing, and capture flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 40 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1be712d5-e733-41ca-aabb-e6287d37d695

📥 Commits

Reviewing files that changed from the base of the PR and between 954d5be and 5132e11.

📒 Files selected for processing (6)
  • src/PS1/runtime/CaptureBuffer.cpp
  • src/PS1/runtime/GpuCommandParser.cpp
  • src/PS1/runtime/GpuCommandParser_test.cpp
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipManager.h
  • src/PS1/runtime/PS1RipWorker.cpp
📝 Walkthrough

Walkthrough

This PR implements Phase 2 of PS1 Runtime Geometry Extraction: a complete GPU primitive and GTE matrix capture infrastructure. It adds data structures for GPU commands, a GP0 parser, matrix deduplication, a per-frame capture buffer, emulator hook interfaces, and worker/manager integration to arm, record, and finalize frame capture with CSV output.

Changes

PS1 Capture Pipeline Implementation

Layer / File(s) Summary
Capture data types and hooks interface
src/PS1/runtime/CaptureTypes.h, src/PS1/runtime/EmuHooks.h
Defines PrimKind, PsxVertex, PrimRecord, DrawModeRecord, Matrix3, MatrixRecord snapshots, and expands EmuHooks with virtual capture lifecycle, matrix, primitive, and VRAM interception methods with default no-op implementations.
GPU command parsing
src/PS1/runtime/GpuCommandParser.h, src/PS1/runtime/GpuCommandParser.cpp, src/PS1/runtime/GpuCommandParser_test.cpp
GpuCommandParser::parseGp0 decodes PS1 GP0 opcodes and packets into PrimRecord and DrawModeRecord for 7 primitive flavors; primsToCsv serializes to CSV. Unit tests validate monochrome/textured/shaded triangles, quads, sprites, and VRAM copy skipping.
GTE matrix capture and deduplication
src/PS1/runtime/GteCapture.h, src/PS1/runtime/GteCapture.cpp
GteCapture::hashMatrix computes 64-bit deterministic hashes for matrix deduplication; matricesEqual provides binary equality checks.
Per-frame capture buffer with matrix deduplication
src/PS1/runtime/CaptureBuffer.h, src/PS1/runtime/CaptureBuffer.cpp, src/PS1/runtime/CaptureBuffer_test.cpp
CaptureBuffer manages frame-scoped primitives, matrices, and draw-modes; deduplicates matrices by hash, tracks per-matrix usage counts, and selects best camera matrix at frame end. Tests verify hash-based deduplication and camera-matrix selection.
Ripper hooks implementation and worker/manager wiring
src/PS1/runtime/RipperHooks.h, src/PS1/runtime/RipperHooks.cpp, src/PS1/runtime/PS1RipWorker.h, src/PS1/runtime/PS1RipWorker.cpp, src/PS1/runtime/PS1RipManager.cpp, src/PS1/runtime/PS1RipManager_test.cpp
RipperHooks implements EmuHooks to conditionally record into CaptureBuffer when armed. PS1RipWorker owns buffer/hooks, wires hooks to core, and provides setCaptureArmed() and finalizeFrameCapture() lifecycle. PS1RipManager connects signals and implements armCapture() and captureFrame() with Sentry logging. Integration test validates armed capture flow.
Stub core capture synthesis for testing
plugins/ps1core_stub/StubCaptureSynth.h, plugins/ps1core_stub/StubCaptureSynth.cpp, plugins/ps1core_stub/StubEmuCore.cpp, plugins/ps1core_stub/CMakeLists.txt
StubCaptureSynth emits one synthetic primitive of each GP0 flavor plus draw-mode per frame when capture enabled, providing deterministic regression test data integrated into StubEmuCore::runFrame().
Build and test infrastructure
src/PS1/CMakeLists.txt, tests/CMakeLists.txt, src/PS1/PS1_RIP_DESIGN.md
Adds new runtime sources/headers to build under ENABLE_PS1_RIP; updates Phase 2 design doc with implementation status and CSV verification notes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • fernandotonon/QtMeshEditor#419: This PR directly implements the GTE matrix capture and deduplication features described in the issue through MatrixRecord structure, hash-based dedupe, and camera-matrix selection logic.
  • fernandotonon/QtMeshEditor#412: This PR implements Phase 2+ components of the PS1 Runtime Geometry Extraction epic, including capture buffer, command parsing, hook interfaces, and end-to-end worker/manager integration.

Possibly related PRs

  • fernandotonon/QtMeshEditor#577: Phase 1 established the stub core plugin and frame loop; this PR extends the plugin with capture synthesis via EmuHooks and wires the entire capture pipeline through the worker.

Poem

🐰 Hop! Data flows from GPU to buffer,
Matrices deduplicated, no more suffer,
Hooks catch each frame's geometry prime,
CSV dumps for verification time!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly specifies Phase 2 GPU/GTE capture hooks and references the linked issues (#418, #419), directly addressing the main implementation.
Description check ✅ Passed The PR description includes a Summary section covering all key implementation areas and a Test plan section. While Technical Details section is missing, the summary adequately covers the general technical areas of change.
Linked Issues check ✅ Passed The implementation fully satisfies issue #418 objectives: EmuHooks interface defined with required callbacks, PrimRecord populated with all required fields, RipperHooks provides concrete capture implementation, capture is opt-in via atomic armed flag with minimal disarmed overhead, GP0 parser decodes all seven primitive flavors, and unit tests verify parsing, matrix deduplication, and end-to-end capture flow.
Out of Scope Changes check ✅ Passed All changes are directly scoped to Phase 2 GPU/GTE capture implementation: new capture infrastructure (CaptureTypes, CaptureBuffer, GpuCommandParser, RipperHooks), integration with EmuHooks interface, stub core emission of test primitives, worker thread wiring, and comprehensive unit tests. No extraneous changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ps1-gpu-gte-capture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e498c2cf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/PS1/runtime/GpuCommandParser.cpp Outdated
Comment thread src/PS1/runtime/GpuCommandParser.cpp Outdated
Comment thread src/PS1/runtime/GpuCommandParser.cpp
fernandotonon and others added 2 commits May 17, 2026 21:03
Parse E1–E6 as single-word drawing-environment commands, fix gouraud
triangle/quad vertex layout per psx-spx, and add 0xC0 to VRAM copy skip sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
src/PS1/runtime/CaptureBuffer.h (1)

18-19: ⚡ Quick win

Use const reference parameters for non-trivial types.

addMatrix and addPrim take their arguments by value, while addDrawMode correctly uses const &. MatrixRecord and PrimRecord are non-trivial types (containing matrices, vertices, colors, etc.), so passing by const reference is both more efficient and consistent with C++ best practices.

♻️ Proposed fix for consistent pass-by-reference
-    uint32_t addMatrix(MatrixRecord matrix);
-    void addPrim(PrimRecord prim);
+    uint32_t addMatrix(const MatrixRecord &matrix);
+    void addPrim(const PrimRecord &prim);
     void addDrawMode(const DrawModeRecord &mode);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/PS1/runtime/CaptureBuffer.h` around lines 18 - 19, Change the addMatrix
and addPrim parameter types to take const references instead of by-value:
replace MatrixRecord matrix with const MatrixRecord& and PrimRecord prim with
const PrimRecord& in the CaptureBuffer.h declarations (and update the
corresponding definitions/implementations in the .cpp to match). Keep
addMatrix's return type (uint32_t) unchanged, and scan/adjust all call sites to
ensure they still compile with the new const-reference signatures (no
ownership/modify semantics assumed); this aligns with addDrawMode's const& usage
and avoids expensive copies of MatrixRecord and PrimRecord.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/PS1/runtime/CaptureBuffer.cpp`:
- Around line 29-31: The current dedup uses only
m_matrixIndexByHash.constFind(matrix.hash) and returns found.value() on a hash
match, risking collision-based mis-association; change the map to store a
list/vector of matrix IDs for each hash (e.g. m_matrixIndexByHash: hash ->
std::vector<MatrixId>), then when you get the bucket for matrix.hash iterate its
IDs, fetch each candidate matrix and compare full equality (use the existing
matrix equality routine or implement a strict CompareMatrices function) and only
return the candidate ID when a true equality match is found; if none match,
insert the new matrix ID into the vector for that hash so future lookups
consider it.

In `@src/PS1/runtime/GpuCommandParser.cpp`:
- Around line 145-146: The textured-quad length check wrongly requires 13 words
causing valid 0x2C..0x2F commands to be rejected; in the GpuCommandParser code
path that handles textured quads (the branch that sets error = "textured quad:
packet too short"), change the guard that uses (index + 13 > wordCount) to
require 9 words instead (e.g., (index + 9 > wordCount)) to match the actual
number of consumed words for those packets; apply the same fix to the second
occurrence noted around the other check (lines that currently use
+13/packet-too-short for 0x2C..0x2F) so both checks use +9 and keep the existing
error message and command-range handling intact.
- Around line 28-34: The function decodeUvTpageClut currently ignores the tpage
parameter (it discards it with (void)tpage), so PrimRecord::tpage never gets
set; modify decodeUvTpageClut to extract and assign tpage from the incoming word
according to the PS1 GP0 TPAGE bit layout and adjust the clut extraction if
necessary so clut and tpage use the correct, non-overlapping bit ranges;
specifically edit decodeUvTpageClut (and any related masks/shifts) to assign
tpage from the appropriate shifted/masked bits of word instead of discarding it
so textured primitives populate PrimRecord::tpage.
- Around line 324-329: The opcode range 0x34..0x37 is missing and so those
commands are being skipped; add an explicit branch for cmd >= 0x34 && cmd <=
0x37 and invoke the appropriate triangle parser (e.g. call parseGouraudTri or
the correct triangle-parsing function used for this variant) with the same
arguments (words, wordCount, index, prim, result.error) so these opcodes produce
primitives like the 0x30..0x33 case; place this new else-if alongside the
existing parseGouraudTri/parseGouraudQuad/parseSprite branches to ensure all
opcode ranges are handled.

In `@src/PS1/runtime/PS1RipManager.cpp`:
- Around line 64-66: Replace the custom breadcrumb categories used for
capture/export with the mandated Sentry categories: change the
SentryReporter::addBreadcrumb calls around the capture flow (the one immediately
before emit frameCaptured(captureId) and the similar calls around lines 201-205)
to use "ui.action" for arm/disarm/toolbar/menu-like capture actions and
"file.export" for frame-save/export operations while preserving the existing
breadcrumb message/payload (e.g., keep "frame_saved:%1" or "capture" text);
locate the SentryReporter::addBreadcrumb invocations in PS1RipManager.cpp and
update their first argument category strings accordingly so they conform to the
coding guidelines.
- Around line 197-213: The manager clears m_captureArmed during
stop/session-stop paths but never updates the worker, so the worker can remain
armed; ensure the worker is explicitly notified when clearing the flag by either
calling PS1RipManager::armCapture(false) from the stop/session-stop code or
invoking the worker update the same way armCapture does (use
QMetaObject::invokeMethod on m_worker to call m_worker->setCaptureArmed(false)
with Qt::QueuedConnection). Update the stop/session-stop code paths to reference
m_worker and setCaptureArmed to propagate the disarm reliably.
- Around line 208-211: In armCapture() and captureFrame(), avoid capturing this
in lambdas queued to m_worker; instead copy m_worker to a local variable (e.g.,
auto worker = m_worker) and capture that pointer by value in the lambda, then
call worker->setCaptureArmed(armed) and worker->captureFrame() respectively;
this ensures the lambda dereferences a stable worker pointer even if the
PS1RipManager is destroyed before the queued call runs.

In `@src/PS1/runtime/PS1RipWorker.cpp`:
- Around line 165-173: QDir().mkpath(dir); building csvPath and creating QFile
currently emits frameCaptureReady regardless of write outcome; change the flow
in PS1RipWorker::(the function using csvPath) so that you only emit
frameCaptureReady(captureId, prims.size()) after confirming the file was
successfully opened and written (i.e., file.open(...) returned true and
file.write(...) succeeded and file.close() completed); if any step fails, do not
emit frameCaptureReady and instead log an error including captureId and csvPath
so failures are visible.

---

Nitpick comments:
In `@src/PS1/runtime/CaptureBuffer.h`:
- Around line 18-19: Change the addMatrix and addPrim parameter types to take
const references instead of by-value: replace MatrixRecord matrix with const
MatrixRecord& and PrimRecord prim with const PrimRecord& in the CaptureBuffer.h
declarations (and update the corresponding definitions/implementations in the
.cpp to match). Keep addMatrix's return type (uint32_t) unchanged, and
scan/adjust all call sites to ensure they still compile with the new
const-reference signatures (no ownership/modify semantics assumed); this aligns
with addDrawMode's const& usage and avoids expensive copies of MatrixRecord and
PrimRecord.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40d1a1cb-d87b-4921-b882-f9fbef12ed08

📥 Commits

Reviewing files that changed from the base of the PR and between 903d6e5 and 954d5be.

📒 Files selected for processing (23)
  • plugins/ps1core_stub/CMakeLists.txt
  • plugins/ps1core_stub/StubCaptureSynth.cpp
  • plugins/ps1core_stub/StubCaptureSynth.h
  • plugins/ps1core_stub/StubEmuCore.cpp
  • src/PS1/CMakeLists.txt
  • src/PS1/PS1_RIP_DESIGN.md
  • src/PS1/runtime/CaptureBuffer.cpp
  • src/PS1/runtime/CaptureBuffer.h
  • src/PS1/runtime/CaptureBuffer_test.cpp
  • src/PS1/runtime/CaptureTypes.h
  • src/PS1/runtime/EmuHooks.h
  • src/PS1/runtime/GpuCommandParser.cpp
  • src/PS1/runtime/GpuCommandParser.h
  • src/PS1/runtime/GpuCommandParser_test.cpp
  • src/PS1/runtime/GteCapture.cpp
  • src/PS1/runtime/GteCapture.h
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipManager_test.cpp
  • src/PS1/runtime/PS1RipWorker.cpp
  • src/PS1/runtime/PS1RipWorker.h
  • src/PS1/runtime/RipperHooks.cpp
  • src/PS1/runtime/RipperHooks.h
  • tests/CMakeLists.txt

Comment thread src/PS1/runtime/CaptureBuffer.cpp Outdated
Comment thread src/PS1/runtime/GpuCommandParser.cpp
Comment thread src/PS1/runtime/GpuCommandParser.cpp Outdated
Comment thread src/PS1/runtime/GpuCommandParser.cpp
Comment thread src/PS1/runtime/PS1RipManager.cpp Outdated
Comment thread src/PS1/runtime/PS1RipManager.cpp
Comment thread src/PS1/runtime/PS1RipManager.cpp Outdated
Comment thread src/PS1/runtime/PS1RipWorker.cpp Outdated
fernandotonon and others added 2 commits May 17, 2026 21:49
Verify matrix equality on hash hits, decode TPAGE from texcoord words,
fix textured quad/gouraud packet sizes, parse 0x34–0x37 opcodes, sync
worker capture armed on stop, use standard Sentry categories, and only
emit frameCaptureReady after a successful CSV write.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud
Copy link
Copy Markdown

@fernandotonon fernandotonon merged commit 384818d into master May 18, 2026
20 checks passed
@fernandotonon fernandotonon deleted the feat/ps1-gpu-gte-capture branch May 18, 2026 02:30
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.

PS1: GPU GP0/GP1 command interception layer

1 participant