Skip to content

feat(ps1): Phase 4 mesh reconstruction from capture (#422)#601

Open
fernandotonon wants to merge 2 commits into
masterfrom
feat/ps1-mesh-reconstruct
Open

feat(ps1): Phase 4 mesh reconstruction from capture (#422)#601
fernandotonon wants to merge 2 commits into
masterfrom
feat/ps1-mesh-reconstruct

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 18, 2026

Summary

  • Add CaptureSnapshot, GteInverse, MeshReconstructor, and PS1RipMeshBuilder to turn captured primitives into Ogre meshes
  • Group by matrixId + texture key; triangulate quads; vertex color + UV; PS1 Y-down → editor Y-up
  • captureFrame imports PS1Capture_<id> into the main scene when the editor is open
  • Session toolbar: Arm Capture + Capture Frame; Sentry ps1.rip.mesh.built

Test plan

  • UnitTests --gtest_filter="MeshReconstructorTest.*:GteInverseTest.*"
  • PS1RipManagerTest.ArmedCaptureAccumulatesPrimitives
  • CI Linux with ENABLE_PS1_RIP=ON
  • Manual: main editor open → PS1 Ripper → Arm Capture → Capture Frame → PS1Capture_* node in scene tree

Closes #422 (epic #412 Phase 4).

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features
    • Added automatic mesh reconstruction from PS1 captures, with geometry now displayed in the scene
    • Added "Arm Capture" and "Capture Frame" toolbar actions to the capture session window
    • Added real-time status updates showing mesh build results with vertex and triangle counts

Review Change Stack

Reconstruct captured primitives into Ogre meshes grouped by matrix and
texture, attach PS1Capture nodes to the editor scene, and add Arm/Capture
toolbar controls with unit tests for GTE inverse and MeshReconstructor.

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

coderabbitai Bot commented May 18, 2026

Warning

Rate limit exceeded

@fernandotonon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 47 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: 5171de1b-9ca9-4030-8714-fa0a4eb315a4

📥 Commits

Reviewing files that changed from the base of the PR and between fdf4fe6 and 9e5512c.

📒 Files selected for processing (8)
  • src/PS1/runtime/GteInverse.cpp
  • src/PS1/runtime/GteInverse.h
  • src/PS1/runtime/GteInverse_test.cpp
  • src/PS1/runtime/MeshReconstructor.cpp
  • src/PS1/runtime/MeshReconstructor.h
  • src/PS1/runtime/PS1RipMeshBuilder.cpp
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/PS1RipWorker.cpp
📝 Walkthrough

Walkthrough

This PR implements the end-to-end PS1 mesh reconstruction pipeline: capturing frame primitives into thread-safe snapshots, transforming screen-space coordinates to world-space, reconstructing geometry grouped by matrix and texture, attaching Ogre meshes to the live scene, and integrating user actions via the session toolbar.

Changes

PS1 Runtime Geometry Extraction and Mesh Attachment

Layer / File(s) Summary
Snapshot Data Contract and Worker Transfer
src/PS1/runtime/CaptureSnapshot.h, src/PS1/runtime/CaptureSnapshot.cpp, src/PS1/runtime/PS1RipWorker.h, src/PS1/runtime/PS1RipWorker.cpp
CaptureSnapshot struct holds thread-safe copies of primitives, matrices, and camera matrix ID. Worker emits frameCaptureReady signal passing a CaptureSnapshot constructed from the current CaptureBuffer alongside capture ID and primitive count.
Coordinate Transformation (GteInverse)
src/PS1/runtime/GteInverse.h, src/PS1/runtime/GteInverse.cpp, src/PS1/runtime/GteInverse_test.cpp
GteInverse namespace provides screenToModel (matrix-based screen-to-model conversion with fixed-scale fallback) and psxScreenToWorld (hardcoded center/scale with Y-down to Y-up flip). Unit test validates Y-axis behavior.
Mesh Reconstruction from Snapshots
src/PS1/runtime/MeshReconstructor.h, src/PS1/runtime/MeshReconstructor.cpp, src/PS1/runtime/MeshReconstructor_test.cpp
MeshReconstructor::reconstruct converts snapshots into ReconstructedMesh by grouping primitives by matrixId and texture key, transforming positions via GteInverse, computing UVs, triangulating quads, and accumulating vertex/index data. Three unit tests cover grouping, quad triangulation, and empty snapshot handling.
Ogre Mesh Attachment (PS1RipMeshBuilder)
src/PS1/runtime/PS1RipMeshBuilder.h, src/PS1/runtime/PS1RipMeshBuilder.cpp
PS1RipMeshBuilder::attachToScene converts reconstructed geometry into Ogre manual mesh with per-submesh vertex buffers (position, normal, UV, diffuse color) and 32-bit index buffers. Creates capture-specific SceneNode and Entity in the editor's scene graph.
Manager Orchestration and Signal Routing
src/PS1/runtime/PS1RipManager.h, src/PS1/runtime/PS1RipManager.cpp
Manager registers CaptureSnapshot as Qt metatype, handles frameCaptureReady by reconstructing mesh and attaching to scene, logs Sentry breadcrumb with vertex/triangle counts, and emits new meshBuilt signal.
Session Window UI Integration
src/PS1/runtime/PS1RipSessionWindow.h, src/PS1/runtime/PS1RipSessionWindow.cpp
Adds "Arm Capture" (toggles arming) and "Capture Frame" (triggers capture with breadcrumb) toolbar actions. New onMeshBuilt handler updates status bar with capture ID and mesh statistics.
Build Configuration
src/PS1/CMakeLists.txt, tests/CMakeLists.txt
Registers new runtime sources (CaptureSnapshot.cpp, GteInverse.cpp, MeshReconstructor.cpp, PS1RipMeshBuilder.cpp) and headers in build when ENABLE_PS1_RIP is active. Test build includes these sources.
Design Documentation
src/PS1/PS1_RIP_DESIGN.md
Documents Phase 4: snapshot capture, GTE inverse coordinate correction, mesh reconstruction grouped by matrix/texture, Ogre mesh building, scene attachment, automated capture in captureFrame, session toolbar actions, and Sentry breadcrumbs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • fernandotonon/QtMeshEditor#422: This PR directly implements the acceptance criteria for mesh reconstruction from captured primitives, including grouping by matrix, coordinate transformation, triangulation, Ogre mesh building, and Sentry breadcrumbs.
  • fernandotonon/QtMeshEditor#419: Implements matrix-based GTE inverse (screenToModel) and snapshot data flow required for un-projecting screen-space primitives using captured transformation matrices.
  • fernandotonon/QtMeshEditor#412: Realizes concrete pipeline components (snapshot, coordinate math, reconstruction, mesh building, manager integration, UI) that were designed in the parent PS1 runtime extraction epic.

Possibly related PRs

  • fernandotonon/QtMeshEditor#581: Phase-2 capture infrastructure that introduced CaptureBuffer and the original two-parameter frameCaptureReady signal; this PR extends that signal to pass a CaptureSnapshot and builds the downstream reconstruction/mesh pipeline.

Poem

🐰 A rabbit hops through PS1 pixels,
Snapshots captured, coordinates flipped,
Matrices group the scattered vessels,
Ogre meshes built and scene-script equipped!
From screen-space depths to world-space heights,
Mesh reconstruction shines in editor lights! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.24% 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 title 'feat(ps1): Phase 4 mesh reconstruction from capture (#422)' accurately describes the main change: adding Phase 4 mesh reconstruction functionality for PS1 capture, directly matching the PR objectives.
Description check ✅ Passed The description covers summary, technical details, test plan with specific filter names, and closure of issue #422. It follows the template structure with summary and technical areas identified.
Linked Issues check ✅ Passed The PR successfully implements all five linked issue requirements: CaptureSnapshot for thread-safe snapshots [#422], GteInverse for coordinate conversion PS1 Y-down to Y-up [#422], MeshReconstructor grouping by matrixId/texture and triangulating quads [#422], PS1RipMeshBuilder attaching meshes to scene [#422], and Sentry logging with vertex/triangle counts [#422].
Out of Scope Changes check ✅ Passed All changes align with Phase 4 mesh reconstruction objectives: CaptureSnapshot, GteInverse, MeshReconstructor, PS1RipMeshBuilder, manager/worker/UI integration, and test infrastructure updates are directly scoped to the linked issue requirements.

✏️ 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-mesh-reconstruct

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: fdf4fe6ee5

ℹ️ 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/MeshReconstructor.cpp Outdated
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: 7

🧹 Nitpick comments (2)
src/PS1/runtime/MeshReconstructor.cpp (2)

91-100: ⚡ Quick win

Sprite reconstruction uses hardcoded offset and may produce incorrect geometry.

The sprite is created by duplicating vertices and adding a fixed 0.05f Y offset, resulting in a thin strip rather than a proper quad. PS1 sprites typically have width/height data that should determine the second pair of vertices. This hardcoded offset won't scale with sprite dimensions.

Is this intentional placeholder geometry, or should sprite dimensions be extracted from PrimRecord fields (e.g., width/height stored in unused vertex slots or separate fields)?

🤖 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/MeshReconstructor.cpp` around lines 91 - 100, The sprite
branch in MeshReconstructor (the PrimKind::Sprite block using prim.vertexCount,
vtx(), ReconstructedVertex and acc.addTriangle) uses a fixed 0.05f Y offset to
create a quad, which is wrong — replace the hardcoded offset with proper sprite
dimensions: read width/height from the PrimRecord (or from the unused vertex
slots if your format stores size there), compute the four quad vertices around
the sprite center using those width/height values (and respect sprite
orientation if available), and then call acc.addTriangle on the two triangles
built from those computed vertices instead of duplicating a/b and adding 0.05f.
Ensure you use the original vertex position/normal/uv when constructing the new
corners so scale and texture mapping stay correct.

125-125: 💤 Low value

Remove unused subIndex variable.

The variable subIndex is incremented but never read, resulting in dead code.

🧹 Proposed fix
-    int subIndex = 0;
     for (auto matIt = groupsByMatrix.constBegin(); matIt != groupsByMatrix.constEnd(); ++matIt) {
         for (auto texIt = matIt.value().constBegin(); texIt != matIt.value().constEnd(); ++texIt) {
             const SubMeshAccumulator &acc = texIt.value();
             if (acc.vertices.isEmpty() || acc.indices.size() < 3)
                 continue;

             ReconstructedSubMesh sub;
             sub.materialName = acc.materialName;
             sub.vertices = acc.vertices;
             sub.indices = acc.indices;
             result.subMeshes.append(sub);
             result.vertexCount += acc.vertices.size();
             result.triangleCount += acc.indices.size() / 3;
-            ++subIndex;
         }
     }

Also applies to: 139-139

🤖 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/MeshReconstructor.cpp` at line 125, Remove the unused dead
variable `subIndex` from MeshReconstructor.cpp: delete the declaration "int
subIndex = 0;" and any subsequent increments or references to `subIndex` (the
occurrences around the current locations ~125 and ~139) so the code no longer
contains an incremented-but-never-read variable; ensure no other logic depends
on `subIndex` after removal.
🤖 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/GteInverse_test.cpp`:
- Around line 5-15: Add a deterministic Google Test unit that exercises
GteInverse::screenToModel (not only psxScreenToWorld): construct a non-trivial
input with a non-zero translation/offset and known projection parameters, call
GteInverse::screenToModel, then assert the outputs are finite (use
std::isfinite) and match expected values within a small tolerance; include at
least one case that verifies the inverse projection returns a non-zero model
coordinate and one deterministic comparison (EXPECT_NEAR) to validate
correctness. Reference the GteInverse::screenToModel function and keep the test
in the existing GteInverse_test.cpp file following the naming pattern
TEST(GteInverseTest, ScreenToModelDeterministic). Ensure you set up any required
projection/state the same way the implementation expects before calling
screenToModel.

In `@src/PS1/runtime/GteInverse.cpp`:
- Around line 18-27: The inverse unprojection currently applies only the
rotation part (matrix.rt) to ir[] and then scales to mx/my/mz, omitting the
translation part; update the computation in GteInverse.cpp to incorporate the
matrix translation (e.g., matrix.tr or TR field) into vx,vy,vz before applying
kFixedScale so screenToModel respects RT/TR/OFX/OFY/H. Specifically, after
accumulating the rotation terms (the loop using matrix.rt.m[c][0..2] and ir[c]),
add the translation components (matrix.tr.x/y/z or matrix.TR[0..2]) to vx, vy,
vz (with the correct sign if conventions require) and then compute mx/my/mz as
before using kFixedScale. Ensure references to mx, my, mz, matrix.rt and
matrix.tr are updated consistently.

In `@src/PS1/runtime/MeshReconstructor.h`:
- Around line 14-16: The default normal components in MeshReconstructor (nx, ny,
nz) are incorrectly initialized with ny = 1.0f, which biases lighting; change
the default values so nx, ny, nz are all 0.0f to match the Phase 4 mesh
contract. Locate the variables nx, ny, nz in MeshReconstructor.h and set their
initializers to 0.0f (keeping the same variable names) so the mesh normals are
zeroed by default.

In `@src/PS1/runtime/PS1RipMeshBuilder.cpp`:
- Around line 145-151: When entity creation fails (entity == nullptr) the
function currently returns false without populating errorOut; update the return
path to set an explanatory message in errorOut (e.g., "failed to create entity
for mesh" plus any context like node or mesh id) before returning, and if
resultOut is expected to be used on failure ensure it is either cleared or not
written; modify the block around resultOut/return to assign errorOut->message
(or appropriate field) when entity is null so callers receive a non-empty error.
- Around line 29-33: The material enables lighting while the mesh builder uses a
uniform normal, causing incorrect shading: change the material setup in
PS1RipMeshBuilder (the code that gets Ogre::Pass via
mat->getTechnique(0)->getPass(0) and calls
pass->setLightingEnabled(true)/setVertexColourTracking(...)) to disable lighting
for reconstructed PS1 capture materials (call setLightingEnabled(false)) or
alternatively compute and assign proper per-face/per-vertex normals in the mesh
construction routines so lighting is correct; also wrap all PS1-specific code in
this file (including PS1RipMeshBuilder class/functions and related material
setup) with `#ifdef` ENABLE_PS1_RIP / `#endif` to follow the optional feature
dependency guideline.

In `@src/PS1/runtime/PS1RipSessionWindow.cpp`:
- Around line 56-60: The "Arm Capture" QAction (armCapAct) can remain checked
when PS1RipManager auto-disarms (e.g., on session stop); add a connection from
PS1RipManager's relevant state-change signal (e.g., a signal such as
armedChanged(bool) or sessionStopped()) to update the QAction checked state so
the UI follows the manager. Concretely, after creating armCapAct and connecting
its toggled->m_manager->armCapture, also connect m_manager's disarm/session-stop
signal to a slot or lambda that calls armCapAct->setChecked(false) (or
setChecked(armed) if an armedChanged(bool) exists); apply the same pattern for
the duplicate block around lines 77-82 where another arm action is created.

In `@src/PS1/runtime/PS1RipWorker.cpp`:
- Line 190: Add a Sentry breadcrumb immediately before the emit of the snapshot:
call SentryReporter::addBreadcrumb(...) with metadata including captureId and
primCount (use prims.size() as primCount) right before the emit
frameCaptureReady(captureId, CaptureSnapshot::fromBuffer(*m_captureBuffer),
prims.size()) line in PS1RipWorker.cpp so the capture completion is recorded;
ensure the breadcrumb uses an appropriate category (e.g., "capture" or
"user.action") and includes captureId and primCount as string values.

---

Nitpick comments:
In `@src/PS1/runtime/MeshReconstructor.cpp`:
- Around line 91-100: The sprite branch in MeshReconstructor (the
PrimKind::Sprite block using prim.vertexCount, vtx(), ReconstructedVertex and
acc.addTriangle) uses a fixed 0.05f Y offset to create a quad, which is wrong —
replace the hardcoded offset with proper sprite dimensions: read width/height
from the PrimRecord (or from the unused vertex slots if your format stores size
there), compute the four quad vertices around the sprite center using those
width/height values (and respect sprite orientation if available), and then call
acc.addTriangle on the two triangles built from those computed vertices instead
of duplicating a/b and adding 0.05f. Ensure you use the original vertex
position/normal/uv when constructing the new corners so scale and texture
mapping stay correct.
- Line 125: Remove the unused dead variable `subIndex` from
MeshReconstructor.cpp: delete the declaration "int subIndex = 0;" and any
subsequent increments or references to `subIndex` (the occurrences around the
current locations ~125 and ~139) so the code no longer contains an
incremented-but-never-read variable; ensure no other logic depends on `subIndex`
after removal.
🪄 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: a6d1bace-d5f1-4b46-abc5-71443f96b878

📥 Commits

Reviewing files that changed from the base of the PR and between 504a61a and fdf4fe6.

📒 Files selected for processing (19)
  • src/PS1/CMakeLists.txt
  • src/PS1/PS1_RIP_DESIGN.md
  • src/PS1/runtime/CaptureSnapshot.cpp
  • src/PS1/runtime/CaptureSnapshot.h
  • src/PS1/runtime/GteInverse.cpp
  • src/PS1/runtime/GteInverse.h
  • src/PS1/runtime/GteInverse_test.cpp
  • src/PS1/runtime/MeshReconstructor.cpp
  • src/PS1/runtime/MeshReconstructor.h
  • src/PS1/runtime/MeshReconstructor_test.cpp
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipManager.h
  • src/PS1/runtime/PS1RipMeshBuilder.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.h
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/PS1RipSessionWindow.h
  • src/PS1/runtime/PS1RipWorker.cpp
  • src/PS1/runtime/PS1RipWorker.h
  • tests/CMakeLists.txt

Comment thread src/PS1/runtime/GteInverse_test.cpp
Comment thread src/PS1/runtime/GteInverse.cpp
Comment thread src/PS1/runtime/MeshReconstructor.h
Comment thread src/PS1/runtime/PS1RipMeshBuilder.cpp
Comment thread src/PS1/runtime/PS1RipMeshBuilder.cpp Outdated
Comment thread src/PS1/runtime/PS1RipSessionWindow.cpp
Comment thread src/PS1/runtime/PS1RipWorker.cpp
Separate model-space vs screen-space transforms, include TR in GTE inverse,
disable lighting on capture materials, and sync Arm Capture UI on stop.

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

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: Reconstruct meshes from captured primitive stream

1 participant