Skip to content

PS1 epic #412: cleanup pipeline (#428), golden validation (#817), CLI (#431), rigid animation (#429)#844

Merged
fernandotonon merged 5 commits into
masterfrom
feat/ps1-remaining-issues-412
Jul 11, 2026
Merged

PS1 epic #412: cleanup pipeline (#428), golden validation (#817), CLI (#431), rigid animation (#429)#844
fernandotonon merged 5 commits into
masterfrom
feat/ps1-remaining-issues-412

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the four remaining open issues of the PS1 runtime-extraction epic (#412), closing it out. Builds on the in-core capture chain merged in #825.

Closes #428, closes #817, closes #431, closes #429.

#817 — Golden validation of the in-core capture chain

  • Wire the retail-c custom-engine scene into PsxGoldenCapture (scene id + QTMESH_PS1_GOLDEN_RETAIL_C_ISO) — the golden doc referenced it but the code never resolved it.
  • RetailCInCoreGoldenPass: boots a configured custom-engine disc through the real EmuCore/RipperHooks path, reconstructs with stats, asserts the pass bar (tracked>0, tracked+depth≥50%, bounded, non-empty) and prints a doc-ready metric line. Verified on Crash Warped (USA): 262 tris / 786 verts / 77% tracked (RAM-scan legacy = 0% tracked on this title).
  • Documented recorded run + reproduction in golden_captures.md.

#428 — Cleanup pipeline for ripped geometry

  • Zero-area (sliver) triangle cull in MeshReconstructor, plus completed Ps1NormalizerSettings persistence for all cleanup toggles.
  • Surfaced: ripper "Drop slivers" checkbox, MCP remove_zero_area, ps1.rip.cleanup.* breadcrumbs.
  • ScanEngine rules ps1-rip-zero-area (Warning) + ps1-rip-degenerate-uv (Info) so qtmesh scan flags dirty captures. Tests for the cull + 4 scan-rule evaluators.
  • T-junction resolution deferred (the weld pass already merges the coincident verts that cause most capture T-junctions; documented).

#431 — Headless qtmesh ps1 CLI subcommand

#429 — Rigid animation capture from per-frame GTE records

  • Ps1AnimationExtractor (Ogre-free, unit-tested): groups a scene capture's per-frame GTE matrices into one track per moving object.
  • PS1RipMeshBuilder::authorRigidAnimation authors an Ogre::NodeAnimationTrack per object so the ripped motion plays in the editor viewport.
  • In-editor preview only — node-transform tracks don't yet round-trip through the glTF/FBX exporter (skeletal only); documented at every surface. Opt-in toggle (GUI/MCP/CLI). Verified on Crash: --scene 3s --rigid-animation → 87% tracked, tracks authored, no crash.

Testing

  • 203 PS1 + scan unit tests pass locally (incl. 6 new extractor tests, 7 CLI coverage tests, zero-area cull + scan-rule tests).
  • End-to-end CLI captures verified against a retail custom-engine disc on macOS.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added experimental headless PS1 commands for runtime capture and VRAM dumping, including scene export, scripted input, filtering/smoothing, and JSON/stat output.
    • Added optional rigid-animation capture for in-editor preview.
    • Added mesh cleanup controls to drop sliver/zero-area triangles and updated scan findings for zero-area triangles and degenerate UVs.
    • Added Retail C golden capture support, plus new capture tool/UI options for the above cleanups.
  • Documentation
    • Expanded PS1 workflow docs with command examples, Linux/xvfb setup, and updated subcommand guidance.
  • Tests
    • Added unit/coverage tests for the new PS1 command parsing and the extractor/scan rule behavior.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds experimental headless PS1 capture and VRAM-dump commands, configurable zero-area cleanup, rigid animation extraction and preview authoring, retail-c golden validation, and scan rules for position and UV degeneracy metrics.

Changes

PS1 runtime extraction

Layer / File(s) Summary
Headless CLI and MCP capture flow
CLAUDE.md, src/AppLaunchHandler.cpp, src/CLIPipeline.*, src/CLIPipeline_cmdps1_coverage_test.cpp, src/MCPServer.cpp
Adds qtmesh ps1 capture and dump-vram, headless event pumping, scripted input, scene export, validation tests, and MCP options.
Capture cleanup settings and culling
src/PS1/runtime/Ps1CoordinateNormalizer.*, src/PS1/runtime/MeshReconstructor.*, src/PS1/runtime/MeshReconstructionStats.h, src/PS1/runtime/PS1RipManager.cpp, src/PS1/runtime/PS1RipSessionWindow.cpp, src/PS1/runtime/MeshReconstructorTiered_test.cpp
Adds persisted cleanup controls, “Drop slivers” UI, zero-area triangle removal, telemetry, and reconstruction tests.
Rigid animation extraction and authoring
src/PS1/runtime/Ps1AnimationExtractor.*, src/PS1/runtime/PS1RipMeshBuilder.*, src/PS1/CMakeLists.txt, tests/CMakeLists.txt
Extracts deterministic GTE matrix tracks and authors rigid animation clips on captured scene nodes, with unit coverage.
Retail-c golden validation
src/PS1/runtime/PsxGoldenCapture.*, src/PS1/runtime/MeshReconstructorGolden_test.cpp, src/PS1/golden_captures.md
Registers the retail-c scene and ISO, adds in-core golden-pass assertions, and documents the recorded run.
PS1 degeneracy scan rules
src/ScanConfig.*, src/ScanEngine.*, src/ScanEngine_test.cpp
Computes position and UV degeneracy ratios, exposes them in JSON/SARIF, and evaluates configurable scan findings.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLIPipeline::cmdPs1
  participant Manager as PS1RipManager
  participant Qt as Qt event loop
  participant Exporter as MeshImporterExporter::sceneExporter
  CLI->>Manager: boot ISO and BIOS
  CLI->>Qt: pump events during capture
  Manager-->>CLI: meshBuilt or vramDumped
  CLI->>Exporter: export scene or copy VRAM PNG
Loading

Possibly related issues

  • Issue 412: Covers the broader PS1 runtime extraction epic, including cleanup, MCP parity, and CLI functionality.
  • Issue 814: Provides the in-core GTE records consumed by animation extraction and golden validation.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.33% 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 accurately summarizes the main PS1 epic work and the four included areas.
Description check ✅ Passed The PR description covers the main work items and testing, though it does not fully match the repository template headings.
Linked Issues check ✅ Passed The changes align with the requested cleanup, golden validation, CLI, and rigid-animation work, including tests and docs.
Out of Scope Changes check ✅ Passed The changes stay focused on the PS1 epic items and supporting tests/docs, with no obvious unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ps1-remaining-issues-412

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.

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

Copy link
Copy Markdown

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

ℹ️ 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".

editorRotationFromGte(rawRot, rotf);
const Ogre::Matrix3 m3(rotf[0], rotf[1], rotf[2], rotf[3], rotf[4], rotf[5],
rotf[6], rotf[7], rotf[8]);
const double time = static_cast<double>(key.frame) / kFps;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize PS1 animation times to the capture start

When a scene capture starts after the game has already booted or after ps1rip_run_frames, GteRecordEntry::frame is the absolute core frame counter, not a capture-relative index. Using it directly schedules the first key at e.g. frame 600 / 30 = 20s and makes the clip length include all pre-capture time, so autoplayed rigid captures appear frozen/overlong; subtract the minimum captured frame when computing key times and clip length.

Useful? React with 👍 / 👎.

Comment on lines +883 to +885
nodeAnim->addKeyframe(clipName, nodeName, time,
Ogre::Vector3(wx, wy, wz), Ogre::Quaternion(m3),
Ogre::Vector3::UNIT_SCALE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep rigid-animation keys in the normalized node space

When captureRigidAnimation is enabled for captures whose auto-fit/user normalizer scale is not exactly 1, these keyframes overwrite the normalized capture-node transform with raw modelToEditor translation and UNIT_SCALE. The static nodes are placed earlier via composeNodeTransform, so enabling the clip makes animated objects pop to raw PS1 coordinates/size instead of the same layout as the captured mesh; keyframes should apply the same placement/user scale and preserve the node scale.

Useful? React with 👍 / 👎.

fernandotonon and others added 3 commits July 11, 2026 01:28
#817 — golden validation of the in-core capture chain:
- Wire the `retail-c` custom-engine scene into PsxGoldenCapture (scene id +
  QTMESH_PS1_GOLDEN_RETAIL_C_ISO env), which the golden doc referenced but the
  code never resolved.
- Add RetailCInCoreGoldenPass: boots a configured custom-engine disc through
  the real EmuCore/RipperHooks path, reconstructs with stats, asserts the
  unattended-achievable pass bar (tracked>0, tracked+depth>=50%, bounded,
  non-empty) and prints a doc-ready metric line. Verified on Crash Warped
  (USA): 262 tris / 786 verts / 77% tracked — RAM-scan legacy is 0% tracked on
  this title. slabLike is a warning (scene-dependent — needs a gameplay camera,
  a manual step) not a hard gate.
- Widen the test's core-present check to the qtmesh wrapper + rip-fork plugin
  names (it only knew the stock beetle/mednafen names, so it early-returned).
- Document the recorded run + how to reproduce it in golden_captures.md.

#428 — cleanup pipeline for ripped geometry:
- Add a zero-area (sliver) triangle cull to MeshReconstructor, toggled by the
  new Ps1NormalizerSettings.cleanupRemoveZeroArea (+ zeroAreaEpsilon), reported
  via MeshReconstructionStats.zeroAreaTrianglesDropped. Complements the existing
  weld+normals (Smooth) and spike-edge culls.
- Complete Ps1NormalizerSettings persistence: isDefault/save/load/describe now
  cover trackedGeometryOnly, spikeEdgeFactor, cleanupWeldNormals and the two new
  fields (several were previously missing from isDefault).
- Surface it everywhere: "Drop slivers" checkbox in the ripper toolbar, MCP
  ps1rip_capture `remove_zero_area` param, ps1.rip.cleanup.* Sentry breadcrumbs.
- ScanEngine rules ps1-rip-zero-area (Warning) + ps1-rip-degenerate-uv (Info):
  new AssetInfo ratios computed in one geometry walk, config keys, evaluateRules
  emission, rule descriptions, JSON output.
- Tests: ZeroAreaCullDropsSliverTriangles + 4 scan-rule evaluator tests.

T-junction resolution from the issue's wishlist is intentionally deferred — the
weld pass already merges the coincident verts that cause most capture
T-junctions, and a general edge-vs-vertex split is a separate algorithm with no
existing scaffolding. Documented on the issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the CLI half of the GUI/CLI/MCP parity rule for the runtime ripper (the
MCP ps1rip_* tools already shipped):

- `qtmesh ps1 capture <iso> --bios <bios> [--frames N | --scene Ns]
   [--script in.json] [--auto-input] [--tracked-only] [--smooth]
   [--drop-slivers] [--dedupe-strict] [-o out.gltf] [--json]`
- `qtmesh ps1 dump-vram <iso> --bios <bios> [--frames N] -o vram.png [--json]`

Ports the MCP capture pattern: boots the disc through PS1RipManager on its
worker thread, pumps the Qt event loop (the CLI has no exec() loop), drives
capture/scene, and exports the reconstructed scene via
MeshImporterExporter::sceneExporter. initOgreHeadless() first so the
reconstructor has a live scene to attach to. JSON input scripts
([{frame,button}, ...]) give reproducible captures; --auto-input mashes
Start/Cross past menus. The #428 cleanup toggles (tracked-only / smooth /
drop-slivers) are exposed here too. ps1.rip.cli.* Sentry breadcrumbs.

Registered in AppLaunchHandler's subcommand list + CLIPipeline dispatch +
printUsage; ENABLE_PS1_RIP-guarded (clean rebuild-flag message when off).
Xvfb-on-Linux note in CLAUDE.md. 7 arg-validation coverage tests
(build-flag-aware). Verified end-to-end on Crash Warped (USA): capture →
21 tris / 2 meshes → valid glTF, and dump-vram → 181 KB PNG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ships the "rigid / hierarchical motion" case from the issue rewrite (the common
PS1 animation): during a Capture Scene, every frame's in-core GTE records carry
each object's changing (rt,tr) while its object-space verts stay fixed, so
animation reduces to recording one matrix track per object — no vertex diffing.

- Ps1AnimationExtractor (Ogre-free, unit-tested): segments the accumulated
  scene gteRecords into per-object draw runs (seq-contiguous same-matrix runs →
  object-space vertex-set hash identity), collects one matrix key per frame,
  and emits a track per object that moves across >= minFrames frames. 6 tests.
- PS1RipMeshBuilder::authorRigidAnimation: matches each extracted track to its
  capture node by first-frame world translation and authors an
  Ogre::NodeAnimationTrack (translation via GteInverse::modelToEditor, rotation
  via editorRotationFromGte) through NodeAnimationManager, then auto-plays it.
  IN-EDITOR PREVIEW ONLY — node-transform tracks don't round-trip through the
  glTF/FBX exporter yet (skeletal only); documented at every surface.
- Opt-in Ps1NormalizerSettings::captureRigidAnimation (persisted): ripper
  "Rigid anim" toggle, MCP ps1rip_capture `rigid_animation`, CLI
  `--rigid-animation`. Scene-capture only; ps1.rip.anim breadcrumb.

Verified end-to-end on Crash Warped (USA): `qtmesh ps1 capture --scene 3s
--rigid-animation` → 87% tracked, 4 objects, 30 matrices, tracks authored, no
crash, glTF exported (geometry).

Caveat (documented, from #816): GTE matrices are View×Model, so tracks fold in
camera motion unless the scene has a static camera — the v1 target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/ScanEngine_test.cpp (1)

599-614: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a Ps1RipDegenerateUv_QuietUnderThreshold test.

The zero-area rule has both fire-over and quiet-under tests, but degenerate-UV only has the fire-over variant. Adding the symmetric quiet case would catch off-by-one or inverted-threshold regressions in the degenerate-UV rule specifically.

🧪 Suggested test
TEST(ScanEngineTest, EvaluateRules_Ps1RipDegenerateUv_QuietUnderThreshold)
{
    AssetInfo asset;
    asset.relativePath = "clean.glb";
    asset.format = "glb";
    asset.ps1RipDegenerateUvRatio = 0.01; // 1%, below the 10% threshold

    ScanConfig config = ScanConfig::defaults();
    config.ps1RipDegenerateUvPct = 10.0;

    auto findings = ScanEngine::evaluateRules(asset, config);
    for (const auto& f : findings)
        EXPECT_NE(f.rule, "ps1-rip-degenerate-uv");
}
🤖 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/ScanEngine_test.cpp` around lines 599 - 614, Add a symmetric
under-threshold test next to
EvaluateRules_Ps1RipDegenerateUv_FiresOverThreshold, using a degenerate UV ratio
below the configured ps1RipDegenerateUvPct and asserting that no finding has
rule "ps1-rip-degenerate-uv".
🤖 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/CLIPipeline.cpp`:
- Around line 9967-9992: Ensure the signal connections created in the PS1
command flow are disconnected before any return from the relevant scope. Store
the QMetaObject::Connection results from the inCoreHooksState and error
connections, then use a scope guard or equivalent cleanup mechanism covering all
failure and success paths, including start and boot failures, so their lambdas
cannot access expired inCoreActive or sessionError locals.

In `@src/PS1/runtime/PS1RipMeshBuilder.cpp`:
- Around line 799-898: Update authorRigidAnimation to accept and use
placementScale, matching the normalization applied by composeNodeTransform().
Apply auto-fit and user scale/flip to the track’s firstWorld position before
nearest-node matching and to every keyframe translation, and use the normalized
placement scale instead of Ogre::Vector3::UNIT_SCALE when calling
NodeAnimationManager::addKeyframe. Ensure the same normalized transform is used
consistently for matching and authored animation.

In `@src/PS1/runtime/PS1RipSessionWindow.cpp`:
- Around line 325-346: Update the toggled breadcrumb calls for both
removeZeroArea and rigidAnim to use the existing ui.action category and the
ps1_rip_<feature>_on/off message convention used by trackedOnly, smoothMesh, and
strictDedupe; avoid the operational ps1.rip.cleanup.zero_area category.

In `@src/ScanConfig.cpp`:
- Around line 292-295: Register "ps1-rip-zero-area" and "ps1-rip-degenerate-uv"
in PlatformProfileLoader::knownRuleKeys() so filterKnownRuleKeys() preserves
them for applyRuleOverrides() and the corresponding ScanConfig thresholds are
applied.

---

Nitpick comments:
In `@src/ScanEngine_test.cpp`:
- Around line 599-614: Add a symmetric under-threshold test next to
EvaluateRules_Ps1RipDegenerateUv_FiresOverThreshold, using a degenerate UV ratio
below the configured ps1RipDegenerateUvPct and asserting that no finding has
rule "ps1-rip-degenerate-uv".
🪄 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: 93492101-9f1c-4ceb-83fe-7cfd3bf11929

📥 Commits

Reviewing files that changed from the base of the PR and between a5fe6bf and df8a714.

📒 Files selected for processing (28)
  • CLAUDE.md
  • src/AppLaunchHandler.cpp
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CLIPipeline_cmdps1_coverage_test.cpp
  • src/MCPServer.cpp
  • src/PS1/CMakeLists.txt
  • src/PS1/golden_captures.md
  • src/PS1/runtime/MeshReconstructionStats.h
  • src/PS1/runtime/MeshReconstructor.cpp
  • src/PS1/runtime/MeshReconstructorGolden_test.cpp
  • src/PS1/runtime/MeshReconstructorTiered_test.cpp
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.h
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.h
  • src/PS1/runtime/Ps1AnimationExtractor_test.cpp
  • src/PS1/runtime/Ps1CoordinateNormalizer.cpp
  • src/PS1/runtime/Ps1CoordinateNormalizer.h
  • src/PS1/runtime/PsxGoldenCapture.cpp
  • src/PS1/runtime/PsxGoldenCapture.h
  • src/ScanConfig.cpp
  • src/ScanConfig.h
  • src/ScanEngine.cpp
  • src/ScanEngine.h
  • src/ScanEngine_test.cpp

Comment thread src/CLIPipeline.cpp
Comment on lines +9967 to +9992
PS1RipManager* mgr = PS1RipManager::getSingleton();
if (!mgr) { err() << "Error: could not create PS1 ripper" << Qt::endl; return 1; }
if (!mgr->loadBios(biosPath)) { err() << "Error: BIOS load failed" << Qt::endl; return 1; }
if (!mgr->loadIso(isoPath)) { err() << "Error: ISO load failed" << Qt::endl; return 1; }

bool inCoreActive = false;
QObject::connect(mgr, &PS1RipManager::inCoreHooksState, mgr,
[&inCoreActive](bool a) { inCoreActive = a; });
QString sessionError;
QObject::connect(mgr, &PS1RipManager::error, mgr,
[&sessionError](const QString& m) { sessionError = m; });

if (!mgr->start()) { err() << "Error: emulator start failed" << Qt::endl; return 1; }

// Wait for boot (bounded). sessionStarted flips isSessionActive().
{
QElapsedTimer boot; boot.start();
while (!mgr->isSessionActive() && boot.elapsed() < 60000 && sessionError.isEmpty())
ps1CliPump(50);
}
if (!mgr->isSessionActive()) {
err() << "Error: emulator did not boot"
<< (sessionError.isEmpty() ? QString() : QStringLiteral(" — ") + sessionError) << Qt::endl;
return 1;
}
err() << "Booted core '" << mgr->activeCoreId() << "'; in-core hooks: "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant function and nearby connections in src/CLIPipeline.cpp
ast-grep outline src/CLIPipeline.cpp --view expanded | sed -n '1,260p'

echo
echo "---- search for PS1RipManager connections and cmdPs1 ----"
rg -n "PS1RipManager|cmdPs1|inCoreHooksState|sessionError|meshBuilt|QObject::disconnect|qScopeGuard|_exit" src/CLIPipeline.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 10170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the surrounding section around the cited lines with line numbers.
sed -n '9930,10040p' src/CLIPipeline.cpp | cat -n

Repository: fernandotonon/QtMeshEditor

Length of output: 6047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the end of cmdPs1 and the run()/_exit flow to understand whether
# events can still be delivered after cmdPs1 returns.
sed -n '10040,10180p' src/CLIPipeline.cpp | cat -n
echo
sed -n '1440,1615p' src/CLIPipeline.cpp | cat -n

Repository: fernandotonon/QtMeshEditor

Length of output: 14230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect PS1RipManager signal declarations and lifecycle-related methods
# to see whether these signals can still fire after cmdPs1 returns.
ast-grep outline src/PS1/runtime/PS1RipManager.h --view expanded | sed -n '1,260p'
echo
rg -n "inCoreHooksState|error\\(|sessionStarted|meshBuilt|vramDumped|stop\\(|start\\(|loadIso\\(|loadBios\\(" src/PS1/runtime/PS1RipManager.*

Repository: fernandotonon/QtMeshEditor

Length of output: 3296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/PS1/runtime/PS1RipManager.h --view expanded | sed -n '1,260p'
echo
rg -n "inCoreHooksState|error\\(|sessionStarted|meshBuilt|vramDumped|stop\\(|start\\(|loadIso\\(|loadBios\\(" src/PS1/runtime/PS1RipManager.*

Repository: fernandotonon/QtMeshEditor

Length of output: 3296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '100,180p' src/PS1/runtime/PS1RipManager.cpp | cat -n
echo
sed -n '300,340p' src/PS1/runtime/PS1RipManager.cpp | cat -n
echo
sed -n '438,520p' src/PS1/runtime/PS1RipManager.cpp | cat -n

Repository: fernandotonon/QtMeshEditor

Length of output: 9874


Disconnect the PS1 signal handlers before returning
PS1RipManager stays alive for the process, and run() keeps spinning the event loop after cmdPs1() returns. These lambdas capture stack locals, so a late inCoreHooksState or error emission can write through freed memory. Store the connections and disconnect them on every exit path, or wrap them in a scope guard like the meshBuilt handler.

🤖 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/CLIPipeline.cpp` around lines 9967 - 9992, Ensure the signal connections
created in the PS1 command flow are disconnected before any return from the
relevant scope. Store the QMetaObject::Connection results from the
inCoreHooksState and error connections, then use a scope guard or equivalent
cleanup mechanism covering all failure and success paths, including start and
boot failures, so their lambdas cannot access expired inCoreActive or
sessionError locals.

Comment on lines +799 to +898

int PS1RipMeshBuilder::authorRigidAnimation(
const QVector<GteRecordEntry> &records,
const QVector<QPair<QString, Ogre::Vector3>> &placements,
const QString &captureId, const Ps1NormalizerSettings &normalize)
{
const QVector<Ps1MatrixTrack> tracks = Ps1AnimationExtractor::extract(records);
if (tracks.isEmpty() || placements.isEmpty())
return 0;

auto *nodeAnim = NodeAnimationManager::instance();
if (!nodeAnim)
return 0;

// Frame -> playback time. PS1 titles run ~30 fps of distinct geometry
// frames in a Capture Scene; a fixed rate keeps the preview timing sane.
constexpr double kFps = 30.0;

// Determine the clip length from the widest frame span across all tracks.
uint32_t maxFrame = 0;
for (const Ps1MatrixTrack &t : tracks)
for (const Ps1MatrixKey &k : t.keys)
maxFrame = std::max(maxFrame, k.frame);
const double clipLength = std::max(0.001, static_cast<double>(maxFrame) / kFps);

const QString clipName = QStringLiteral("PS1Anim_%1").arg(captureId);
if (!nodeAnim->createClip(clipName, clipLength))
return 0;

// Match each track to the placement node whose world position is nearest
// the track's FIRST-frame world translation (GTE tr -> editor units). Each
// node is claimed at most once.
QVector<bool> claimed(placements.size(), false);
int authored = 0;

for (const Ps1MatrixTrack &track : tracks) {
if (track.keys.isEmpty())
continue;

// First-frame world position of this object.
float fx, fy, fz;
GteInverse::modelToEditor(static_cast<float>(track.keys.first().tr[0]),
static_cast<float>(track.keys.first().tr[1]),
static_cast<float>(track.keys.first().tr[2]),
fx, fy, fz);
const Ogre::Vector3 firstWorld(fx, fy, fz);

int bestIdx = -1;
float bestDist = std::numeric_limits<float>::max();
for (int i = 0; i < placements.size(); ++i) {
if (claimed[i])
continue;
const float d = placements[i].second.squaredDistance(firstWorld);
if (d < bestDist) {
bestDist = d;
bestIdx = i;
}
}
if (bestIdx < 0)
continue;
claimed[bestIdx] = true;
const QString &nodeName = placements[bestIdx].first;

// Author one keyframe per captured frame. Translation via modelToEditor;
// rotation via the same GTE->editor basis the static placement uses.
// Scale stays at the node's current scale (rigid — no per-frame scale).
for (const Ps1MatrixKey &key : track.keys) {
float wx, wy, wz;
GteInverse::modelToEditor(static_cast<float>(key.tr[0]),
static_cast<float>(key.tr[1]),
static_cast<float>(key.tr[2]), wx, wy, wz);
// Fold the auto-fit + user scale/flip into the animated translation
// so the clip lands in the same world layout as the static node.
float rotf[9];
const float rawRot[9] = {
static_cast<float>(key.rt[0]) / 4096.0f, static_cast<float>(key.rt[1]) / 4096.0f,
static_cast<float>(key.rt[2]) / 4096.0f, static_cast<float>(key.rt[3]) / 4096.0f,
static_cast<float>(key.rt[4]) / 4096.0f, static_cast<float>(key.rt[5]) / 4096.0f,
static_cast<float>(key.rt[6]) / 4096.0f, static_cast<float>(key.rt[7]) / 4096.0f,
static_cast<float>(key.rt[8]) / 4096.0f};
editorRotationFromGte(rawRot, rotf);
const Ogre::Matrix3 m3(rotf[0], rotf[1], rotf[2], rotf[3], rotf[4], rotf[5],
rotf[6], rotf[7], rotf[8]);
const double time = static_cast<double>(key.frame) / kFps;
nodeAnim->addKeyframe(clipName, nodeName, time,
Ogre::Vector3(wx, wy, wz), Ogre::Quaternion(m3),
Ogre::Vector3::UNIT_SCALE);
}
++authored;
}

if (authored == 0) {
nodeAnim->deleteClip(clipName);
return 0;
}
// Auto-play the preview so the user sees the ripped motion immediately.
nodeAnim->setClipEnabled(clipName, true);
(void)normalize;
return authored;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check NodeAnimationManager::addKeyframe signature and implementation
ast-grep outline src/NodeAnimationManager.h --match addKeyframe --view expanded
rg -n -A 10 'addKeyframe' src/NodeAnimationManager.h src/NodeAnimationManager.cpp -g '*.{h,cpp}'

Repository: fernandotonon/QtMeshEditor

Length of output: 7978


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- NodeAnimationManager::addKeyframe implementation ---'
sed -n '169,260p' src/NodeAnimationManager.cpp

echo
echo '--- PS1RipMeshBuilder static placement path ---'
rg -n -A 30 -B 20 'composeNodeTransform|placementScale|posOut|scaleOut|authorRigidAnimation|placements' src/PS1/runtime/PS1RipMeshBuilder.cpp -n

echo
echo '--- PS1CoordinateNormalizer::composeNodeTransform declaration/definition ---'
rg -n -A 20 -B 10 'composeNodeTransform' src -g '*.{h,cpp}'

Repository: fernandotonon/QtMeshEditor

Length of output: 36231


🌐 Web query:

Ogre NodeAnimationTrack keyframe translate scale absolute or relative semantics SceneNode animation

💡 Result:

In Ogre3D, NodeAnimationTrack is a specialized animation track used to animate SceneNode properties, including translation, rotation, and scale [1][2]. Keyframes within a NodeAnimationTrack contain transformation snapshots—specifically position (translation), orientation (rotation), and scale—at discrete points in time [3][4]. When an animation plays, Ogre interpolates between these keyframes (tweening) to produce smooth movement [3][5]. Regarding the semantics of transforms in Ogre animations: 1. Absolute vs. Relative: Ogre NodeAnimationTrack keyframes generally operate in absolute terms relative to the node's parent or local space (effectively defining the node's transform state at a specific time) [4]. They are not inherently relative to the node's previous position or "delta-based" in the track itself [4]. Because they act as absolute snapshots, applying the same animation to different nodes will result in those nodes moving to the exact same world or parent-relative coordinates [4]. 2. Applying Transforms: When you apply an animation to a SceneNode, Ogre uses the apply or applyToNode methods [6][7]. These methods take the interpolated values from the track and update the node's state [3][6]. 3. Scaling and Blending: The apply method supports a scale parameter, which is used to adapt an animation (e.g., scaling the translation/movement intensity) to different target sizes [3][8]. When blending multiple animations, weights are applied to these transforms to determine the final blended state of the node [3][7]. 4. Best Practices: Since these keyframes are absolute snapshots, if you wish to animate a node relative to its current starting position, you must manually factor the node's initial or current position into the keyframe data when creating the animation [4]. Alternatively, you can use methods like setInitialState on a node to establish a base state for delta calculations if the specific animation system implementation supports it [9]. In summary, the semantics are primarily snapshot-based (absolute transform values per keyframe), requiring manual adjustment if "relative" movement (i.e., animation offsets based on starting position) is desired [4].

Citations:


Normalize the rigid-animation keyframes
NodeAnimationTrack uses absolute transforms, so the raw modelToEditor() translation and Ogre::Vector3::UNIT_SCALE here will override the static composeNodeTransform() placement when auto-fit or user scale/flip is enabled. Pass placementScale through and apply the same normalization to both firstWorld matching and each keyframe.

🤖 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/PS1RipMeshBuilder.cpp` around lines 799 - 898, Update
authorRigidAnimation to accept and use placementScale, matching the
normalization applied by composeNodeTransform(). Apply auto-fit and user
scale/flip to the track’s firstWorld position before nearest-node matching and
to every keyframe translation, and use the normalized placement scale instead of
Ogre::Vector3::UNIT_SCALE when calling NodeAnimationManager::addKeyframe. Ensure
the same normalized transform is used consistently for matching and authored
animation.

Comment on lines +325 to +346
// Mesh cleanup: drop zero-area (sliver) triangles from the raw capture
// (#428). Complements Smooth — welding shares verts, this removes the
// collinear / duplicate-vertex triangles PS1 quad-splits leave behind.
auto *removeZeroArea = new QCheckBox(tr("Drop slivers"), this);
removeZeroArea->setToolTip(tr("Clean up: remove zero-area (collinear / duplicate-vertex) "
"sliver triangles common in PS1 captures. Takes effect on "
"the next Capture Frame."));
removeZeroArea->setChecked(settings.value(ps1SettingsKey(kRemoveZeroAreaKey), false).toBool());
{
Ps1NormalizerSettings s = m_manager->normalizerSettings();
s.cleanupRemoveZeroArea = removeZeroArea->isChecked();
m_manager->setNormalizerSettings(s);
}
connect(removeZeroArea, &QCheckBox::toggled, this, [this](bool on) {
Ps1NormalizerSettings s = m_manager->normalizerSettings();
s.cleanupRemoveZeroArea = on;
m_manager->setNormalizerSettings(s);
SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.cleanup.zero_area"),
on ? QStringLiteral("on") : QStringLiteral("off"));
QSettings().setValue(ps1SettingsKey(kRemoveZeroAreaKey), on);
});
toolbar->addWidget(removeZeroArea);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use ui.action breadcrumb category for the new checkbox toggles.

Both new checkboxes (removeZeroArea and rigidAnim) are UI actions but use ps1.rip.cleanup.zero_area and ps1.rip.anim as breadcrumb categories. All existing checkboxes in the same toolbar (trackedOnly, smoothMesh, strictDedupe) use ui.action with ps1_rip_<feature>_on/off messages. Additionally, ps1.rip.cleanup.zero_area collides with the operational telemetry category in PS1RipManager.cpp line 202, making it hard to distinguish the user toggle from the capture-time drop count in Sentry.

As per coding guidelines, use ui.action for UI actions.

♻️ Proposed fix for breadcrumb categories
     connect(removeZeroArea, &QCheckBox::toggled, this, [this](bool on) {
         Ps1NormalizerSettings s = m_manager->normalizerSettings();
         s.cleanupRemoveZeroArea = on;
         m_manager->setNormalizerSettings(s);
-        SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.cleanup.zero_area"),
-                                      on ? QStringLiteral("on") : QStringLiteral("off"));
+        SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                      on ? QStringLiteral("ps1_rip_remove_zero_area_on")
+                                         : QStringLiteral("ps1_rip_remove_zero_area_off"));
         QSettings().setValue(ps1SettingsKey(kRemoveZeroAreaKey), on);
     });
     connect(rigidAnim, &QCheckBox::toggled, this, [this](bool on) {
         Ps1NormalizerSettings s = m_manager->normalizerSettings();
         s.captureRigidAnimation = on;
         m_manager->setNormalizerSettings(s);
-        SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.anim"),
-                                      on ? QStringLiteral("capture_on")
-                                         : QStringLiteral("capture_off"));
+        SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                      on ? QStringLiteral("ps1_rip_rigid_anim_on")
+                                         : QStringLiteral("ps1_rip_rigid_anim_off"));
         QSettings().setValue(ps1SettingsKey(kRigidAnimKey), on);
     });

Also applies to: 348-371

🤖 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/PS1RipSessionWindow.cpp` around lines 325 - 346, Update the
toggled breadcrumb calls for both removeZeroArea and rigidAnim to use the
existing ui.action category and the ps1_rip_<feature>_on/off message convention
used by trackedOnly, smoothMesh, and strictDedupe; avoid the operational
ps1.rip.cleanup.zero_area category.

Source: Coding guidelines

Comment thread src/ScanConfig.cpp
Comment on lines +292 to +295
if (r.contains("ps1-rip-zero-area"))
ps1RipZeroAreaPct = r["ps1-rip-zero-area"].toDouble();
if (r.contains("ps1-rip-degenerate-uv"))
ps1RipDegenerateUvPct = r["ps1-rip-degenerate-uv"].toDouble();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the knownRuleKeys function definition and check for the new keys.
rg -n 'knownRuleKeys' --type=cpp -C5 src/

Repository: fernandotonon/QtMeshEditor

Length of output: 3208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full knownRuleKeys implementation and nearby context.
sed -n '114,220p' src/PlatformProfile.cpp

printf '\n---\n'

# Inspect the ScanConfig declarations and assignments for the two keys.
rg -n -C 3 'ps1-rip-zero-area|ps1-rip-degenerate-uv' src/ScanConfig.cpp src/ScanConfig.h src/PlatformProfile.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 6592


Register ps1-rip-zero-area and ps1-rip-degenerate-uv in PlatformProfileLoader::knownRuleKeys()
filterKnownRuleKeys() drops them before applyRuleOverrides(), so profile values for these ScanConfig thresholds are silently ignored.

🤖 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/ScanConfig.cpp` around lines 292 - 295, Register "ps1-rip-zero-area" and
"ps1-rip-degenerate-uv" in PlatformProfileLoader::knownRuleKeys() so
filterKnownRuleKeys() preserves them for applyRuleOverrides() and the
corresponding ScanConfig thresholds are applied.

@fernandotonon fernandotonon force-pushed the feat/ps1-remaining-issues-412 branch from df8a714 to c146cf3 Compare July 11, 2026 05:51
fernandotonon and others added 2 commits July 11, 2026 02:02
The convenience `extract(records)` overload was defined inline in the header
and forwarded to `extract(records, Options{})`. CI's unit-tests-linux hit an
undefined reference to the 2-arg overload from the header site. Move the
single-arg definition into the .cpp so there is exactly one emitted definition
and no header-side dependency on the nested Options initializers. Builds +
links clean; 6 extractor tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-file test harness in tests/CMakeLists.txt (qtmesh_test_common →
MaterialEditorQML_test etc.) has its OWN explicit PS1 runtime source list,
separate from src/PS1/CMakeLists.txt. PS1RipMeshBuilder.cpp now calls
Ps1AnimationExtractor::extract, so that list needs the extractor TU too —
without it those executables fail to link with an undefined reference
(the CI unit-tests-linux failure). Verified: both extract() overloads are now
defined (T) in libqtmesh_test_common.a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/golden_captures.md`:
- Around line 90-92: Specify a language on the fenced code block containing the
retail-c golden output by changing its opening fence to use text (or another
appropriate language), satisfying MD040.
- Around line 85-87: Align the golden-scene assertion and documentation on the
same pass criterion: require depth-valid coverage of at least 50% (or explicitly
revise the table if the combined tracked-plus-depth metric is intended). Update
the test description, assertions, and recorded example consistently, using the
golden capture test and its tracked/depth pass-bar logic as the reference.
- Around line 128-133: Update the retail-c table entry and its surrounding
explanation to explicitly label the automated capture as diagnostic
evidence/non-pass, since it has depth=0% and slabLike=1. Retain the in-core
recovery details, but state that acceptance requires a static gameplay capture
with !slabLike and sufficient depth validity; revise the “both legitimate”
wording so it does not imply the current rows satisfy the documented golden-run
criteria.
🪄 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: 628411c2-3326-4fde-9af4-bde56b6d97f1

📥 Commits

Reviewing files that changed from the base of the PR and between df8a714 and f0c75c2.

📒 Files selected for processing (29)
  • CLAUDE.md
  • src/AppLaunchHandler.cpp
  • src/CLIPipeline.cpp
  • src/CLIPipeline.h
  • src/CLIPipeline_cmdps1_coverage_test.cpp
  • src/MCPServer.cpp
  • src/PS1/CMakeLists.txt
  • src/PS1/golden_captures.md
  • src/PS1/runtime/MeshReconstructionStats.h
  • src/PS1/runtime/MeshReconstructor.cpp
  • src/PS1/runtime/MeshReconstructorGolden_test.cpp
  • src/PS1/runtime/MeshReconstructorTiered_test.cpp
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.h
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.h
  • src/PS1/runtime/Ps1AnimationExtractor_test.cpp
  • src/PS1/runtime/Ps1CoordinateNormalizer.cpp
  • src/PS1/runtime/Ps1CoordinateNormalizer.h
  • src/PS1/runtime/PsxGoldenCapture.cpp
  • src/PS1/runtime/PsxGoldenCapture.h
  • src/ScanConfig.cpp
  • src/ScanConfig.h
  • src/ScanEngine.cpp
  • src/ScanEngine.h
  • src/ScanEngine_test.cpp
  • tests/CMakeLists.txt
✅ Files skipped from review due to trivial changes (4)
  • src/PS1/runtime/PsxGoldenCapture.h
  • src/PS1/runtime/PS1RipManager.cpp
  • src/CLIPipeline.h
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (23)
  • src/PS1/runtime/MeshReconstructionStats.h
  • src/AppLaunchHandler.cpp
  • src/ScanConfig.cpp
  • src/ScanEngine_test.cpp
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.h
  • src/PS1/runtime/Ps1CoordinateNormalizer.cpp
  • src/PS1/CMakeLists.txt
  • src/PS1/runtime/Ps1AnimationExtractor_test.cpp
  • src/PS1/runtime/Ps1CoordinateNormalizer.h
  • src/ScanEngine.h
  • src/ScanConfig.h
  • src/PS1/runtime/PsxGoldenCapture.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.h
  • src/MCPServer.cpp
  • src/PS1/runtime/MeshReconstructorTiered_test.cpp
  • src/PS1/runtime/MeshReconstructor.cpp
  • src/PS1/runtime/PS1RipMeshBuilder.cpp
  • src/ScanEngine.cpp
  • src/PS1/runtime/Ps1AnimationExtractor.cpp
  • src/CLIPipeline_cmdps1_coverage_test.cpp
  • src/PS1/runtime/MeshReconstructorGolden_test.cpp
  • src/CLIPipeline.cpp

Comment on lines +85 to +87
It boots the disc, captures one frame through the real `EmuCore`/`RipperHooks`
path, reconstructs with stats, asserts the pass bar (`tracked > 0`,
`tracked+depth ≥ 50%`, `!slabLike`, has bounds), and prints a doc-ready line to

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the automated assertion with the documented pass bar.

The golden-scene table requires depth-valid ≥ 50%, but this section says the test accepts tracked+depth ≥ 50%. The recorded example (tracked=77%, depth=0%) would therefore pass this assertion while failing the stated retail-c criterion. Update the test or documentation so the acceptance threshold is unambiguous.

🤖 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/golden_captures.md` around lines 85 - 87, Align the golden-scene
assertion and documentation on the same pass criterion: require depth-valid
coverage of at least 50% (or explicitly revise the table if the combined
tracked-plus-depth metric is intended). Update the test description, assertions,
and recorded example consistently, using the golden capture test and its
tracked/depth pass-bar logic as the reference.

Comment on lines +90 to +92
```
[retail-c golden] tris=… verts=… tracked=…% depth=…% trusted=…% slabLike=0 hasBounds=1 prims=… meshes=…
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the fenced-block language.

Add text (or another appropriate language) after the opening fence to satisfy MD040.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 90-90: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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/golden_captures.md` around lines 90 - 92, Specify a language on the
fenced code block containing the retail-c golden output by changing its opening
fence to use text (or another appropriate language), satisfying MD040.

Source: Linters/SAST tools

Comment on lines +128 to +133
| 2026-07 | `retail-c` | Crash Bandicoot: Warped (USA) | `924c475` | `tris=262 verts=786 tracked=77% depth=0% trusted=77% slabLike=1 hasBounds=1 prims=248 meshes=4` | **Unattended automated boot** (`RetailCInCoreGoldenPass`, ~600 frames, no controller input). Demonstrates the in-core chain end-to-end on a custom-engine retail disc: **77% of vertices land exact model-space via in-core GTE records** (RAM-scan legacy = 0% tracked on this title). `slabLike=1` because the unattended boot pauses on the 2D title/loading screen — the geometry recovered there is a flat UI plane. Reaching a static 3D **gameplay** camera (where `!slabLike` and the mesh is recognizable — e.g. the "WARPED" title 3D text, level props) requires driving the game by hand; do that pass in the GUI session window and record the numbers + screenshots here. |

The automated row is the **repeatable CI-adjacent proof**; the manual gameplay row
is the **visual-quality proof**. Both are legitimate — the epic bar (#817) allows
partial recovery and asks that what is and isn't recovered be documented, which the
two rows together do.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark this as diagnostic evidence, not a passing golden run.

This row has depth=0% and slabLike=1, so it fails the documented retail-c bar. Keep it as an automated in-core diagnostic, but label it explicitly as non-pass until the required static gameplay capture with !slabLike and sufficient depth validity is recorded; otherwise the “both legitimate” wording can be read as satisfying the acceptance criteria.

🤖 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/golden_captures.md` around lines 128 - 133, Update the retail-c table
entry and its surrounding explanation to explicitly label the automated capture
as diagnostic evidence/non-pass, since it has depth=0% and slabLike=1. Retain
the in-core recovery details, but state that acceptance requires a static
gameplay capture with !slabLike and sufficient depth validity; revise the “both
legitimate” wording so it does not imply the current rows satisfy the documented
golden-run criteria.

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon fernandotonon merged commit 3d4234b into master Jul 11, 2026
21 checks passed
@fernandotonon fernandotonon deleted the feat/ps1-remaining-issues-412 branch July 11, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant