Skip to content

feat(ps1): Phase 1 EmuCore plugin, viewport, and session UI#577

Merged
fernandotonon merged 4 commits into
masterfrom
feat/ps1-emucore-phase1
May 17, 2026
Merged

feat(ps1): Phase 1 EmuCore plugin, viewport, and session UI#577
fernandotonon merged 4 commits into
masterfrom
feat/ps1-emucore-phase1

Conversation

@fernandotonon
Copy link
Copy Markdown
Owner

@fernandotonon fernandotonon commented May 17, 2026

Summary

PS1RipWorker runs the core on its QThread at ~60 Hz; main thread receives framePresented via queued signals.

Test plan

  • cmake -DENABLE_PS1_RIP=ON -DBUILD_TESTS=ON builds app + qtmesh_ps1core_stub + UnitTests
  • UnitTests --gtest_filter="EmuCoreLoader*:PS1RipManager*" — 7 passed
  • CI Linux/Windows/macOS
  • Manual: Tools → Experimental → PS1 Runtime Ripper… → load BIOS + ISO → Start (stub animates)

Closes #415, #416, #417. Epic #412.

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • PS1 Runtime Ripper session window now accessible via Tools → Experimental menu
    • Load BIOS and ISO files for emulation sessions
    • Playback controls: start, stop, pause, step, and reset
    • Emulator output display with FPS counter and integer scaling options
  • Documentation

    • Updated design documentation for PS1 runtime feature with current implementation status

Review Change Stack

Introduce EmuCore/IEmuCorePlugin with QPluginLoader discovery, a stub core
plugin for CI, worker-thread frame loop, EmuViewport session window, and
first-run legality dialog. Tools > Experimental opens the ripper when
ENABLE_PS1_RIP is on.

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

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete PS1 Runtime Ripper system with pluggable emulator cores. It defines abstract EmuCore and plugin interfaces, implements a stub core that renders test patterns, creates a worker thread to manage frame execution, builds a manager for session lifecycle control, and delivers a Qt session window with viewport rendering, BIOS/ISO file picking, and playback controls.

Changes

PS1 Runtime Ripper Plugin Architecture & Session UI

Layer / File(s) Summary
Plugin Architecture Contracts
src/PS1/runtime/EmuCore.h, EmuFramebuffer.h, EmuHooks.h, IEmuCorePlugin.h
Defines abstract interfaces for pluggable emulator cores including framebuffer snapshots, hook callbacks, and plugin factory contracts so cores can be swapped without recompiling the main binary.
Plugin Loader and Discovery
src/PS1/runtime/EmuCoreLoader.h, EmuCoreLoader.cpp, EmuCoreLoader_test.cpp
Static loader service discovers and dynamically instantiates EmuCore plugins from PS1Cores/ directories using Qt's QPluginLoader, computes OS-specific search paths, validates plugin interfaces, and tests verify path discovery and stub core loading.
Stub Emulator Core Plugin
plugins/ps1core_stub/CMakeLists.txt, StubEmuCore.h, StubEmuCore.cpp, StubEmuCorePlugin.h, StubEmuCorePlugin.cpp, ps1core_stub.json
Minimal EmuCore implementation generating a 320×240 deterministic RGB test pattern with frame indexing and BIOS/ISO path validation; built as a Qt plugin shared library with metadata and build configuration so EmuCoreLoader can instantiate it at runtime.
Worker Thread Emulation Engine
src/PS1/runtime/PS1RipWorker.h, PS1RipWorker.cpp
QObject-based worker managing emulation lifecycle on a dedicated QThread with 16ms frame timing via QTimer, core loading and BIOS/ISO setup, pause/step/start/stop control via queued signals, framebuffer-to-QImage conversion, and framePresented emission carrying rendered images plus frame indices.
Session Manager and Lifecycle Control
src/PS1/runtime/PS1RipManager.h, PS1RipManager.cpp, PS1RipManager_test.cpp
Singleton coordinator handling BIOS/ISO file loading with validation, session start/stop/pause state machines, queued worker thread IPC, settings persistence, Sentry breadcrumbs, and test coverage of lifecycle transitions including start-pending cancellation and idempotency.
Framebuffer Viewport Widget
src/PS1/runtime/EmuViewport.h, EmuViewport.cpp
Qt QWidget rendering framebuffer QImage snapshots with configurable integer scaling or aspect-ratio scaling, conditional texture smoothing, FPS overlay text, and "No frame" placeholder, updating on frame/scaling/FPS changes and window resize.
Session UI Window and Legal Acknowledgment
src/PS1/runtime/PS1RipSessionWindow.h, PS1RipSessionWindow.cpp, PS1RipLegalityDialog.h, PS1RipLegalityDialog.cpp
PS1RipSessionWindow QMainWindow hosts EmuViewport, toolbar with BIOS/ISO file pickers and transport controls, status label, FPS tracking, and frame/error handlers; PS1RipLegalityDialog enforces one-time user acknowledgment via QSettings with Sentry breadcrumbs and supports recent ISO path history persistence.
Build Configuration and Runtime Integration
CMakeLists.txt, src/CMakeLists.txt, src/PS1/CMakeLists.txt, tests/CMakeLists.txt, src/mainwindow.cpp, CLAUDE.md, src/PS1/PS1_RIP_DESIGN.md
CMake build scripts conditionally compile stub core as a Qt6 module library, inject PS1 runtime sources when ENABLE_PS1_RIP is set, add test dependencies on the stub plugin, wire the Tools menu action to PS1RipSessionWindow::showSession(), and update documentation to reflect Phase 1 completion with plugin architecture and legality safeguards.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #415: PR directly implements the EmuCore plugin architecture, stub core loading via QPluginLoader, worker-thread emulation scheduling, and framebuffer snapshot acceptance criteria from this feature.
  • #417: PR implements the PS1 Runtime Ripper session UI (PS1RipSessionWindow), BIOS/ISO picker integration, legality acknowledgment dialog, Sentry breadcrumbs, and session manager lifecycle control described in the issue.
  • #416: PR delivers the EmuViewport widget for framebuffer rendering, 60Hz emulation loop via QTimer in PS1RipWorker, and session window frame presentation integration specified in the issue.
  • #430: Both PRs modify emulator-core abstractions; this PR adds PS1-specific EmuCore/EmuHooks and plugin infrastructure, aligning with the goal to generalize the abstraction for multiple console support.

Poem

🐰 A plugin awakens, test patterns dance bright,
BIOS and ISO files load through the night,
Worker threads frame, viewport renders with care,
PS1 ripper blooms in the session window's glare! 🎮✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.95% 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 clearly summarizes the main change: Phase 1 implementation of EmuCore plugin infrastructure, viewport widget, and session UI for PS1 runtime ripping.
Description check ✅ Passed The PR description includes a summary section covering the main components (#415, #416, #417), technical details about PS1RipWorker threading, and a test plan. However, it lacks a formal 'Technical Details' section with the bullets structure specified in the template.
Linked Issues check ✅ Passed The PR implements all core objectives from #415: EmuCore abstract API, IEmuCorePlugin/EmuCoreLoader with QPluginLoader from PS1Cores/, stub plugin with test pattern, and worker-thread execution. #416 and #417 objectives (EmuViewport, legality dialog, BIOS/ISO pickers, toolbar, persistence) are all implemented. Acceptance criteria are met: interface + factory landed, stub plugin builds/loads, test coverage added, and no GPL code in main binary.
Out of Scope Changes check ✅ Passed All changes are scoped to the Phase 1 implementation: EmuCore infrastructure, stub plugin, EmuViewport, PS1RipSessionWindow, legality dialog, and worker integration. Modifications to existing files (PS1RipManager, PS1RipWorker, mainwindow.cpp) are direct implementations of the linked issue requirements. No unrelated refactoring or feature creep detected.

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

✨ 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-emucore-phase1

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: 5333e3bca2

ℹ️ 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/PS1RipSessionWindow.cpp Outdated
Comment thread src/PS1/runtime/PS1RipManager.cpp Outdated
fernandotonon and others added 2 commits May 17, 2026 13:53
Native QFileDialog can hang when opened from the Ogre-hosted main window
event loop. Match the rest of the app with DontUseNativeDialog and defer
restoring the saved BIOS path until after the window is shown.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cancel pending startup via an atomic flag checked after core load so Stop
before the worker finishes no longer leaves a session running. Reset waits
for sessionStopped before restarting. Link PS1 runtime sources into
MaterialEditorQML test targets when ENABLE_PS1_RIP is on.

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

🤖 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 `@plugins/ps1core_stub/StubEmuCore.cpp`:
- Around line 53-55: The reset leaves stale per-buffer phase because
fillTestPattern reads buf.frameIndex while reset() only resets m_frameIndex; fix
by initializing the selected write buffer's frame state before regenerating
pixels: set the write buffer's frameIndex (or equivalent phase/state in
m_buffers[m_writeIndex]) to m_frameIndex (or 0) prior to calling
fillTestPattern, and then set m_readIndex = m_writeIndex so the displayed frame
uses the new phase; update StubEmuCore::reset() to adjust
m_buffers[m_writeIndex]'s phase field (or call a helper to sync buffer state) so
no old phase remains.

In `@src/PS1/runtime/EmuCoreLoader_test.cpp`:
- Around line 49-51: Replace the runtime skip with a hard test failure so
missing plugin fails CI: in EmuCoreLoader_test.cpp where the code currently does
"if (!found) { GTEST_SKIP() << ...; }", change it to assert the prerequisite
(e.g., use ASSERT_TRUE(found) << "PS1 stub core plugin not built beside test
binary"; or alternatively FAIL() << "PS1 stub core plugin not built beside test
binary";) so the test fails loudly when the plugin is missing.

In `@src/PS1/runtime/EmuCoreLoader.cpp`:
- Around line 11-14: bundleOrAppDir() currently returns
QCoreApplication::applicationDirPath(), which points to Contents/MacOS inside
.app bundles; change it to return the bundle root on macOS by using
QCoreApplication::applicationDirPath() fallback to
QCoreApplication::applicationDirPath() only on non-macOS and use
QCoreApplication::applicationDirPath() replacement
QCoreApplication::macBundlePath() under a Q_OS_MAC (or `#ifdef` Q_OS_MAC) guard so
PS1Cores and any code referencing bundleOrAppDir() (also update the similar
logic around the other occurrence at the 31-37 region) resolve resources
relative to the .app bundle root as required by resources.cfg.
- Around line 16-26: The plugin discovery currently hardcodes only the
"qtmesh_ps1core_stub" filenames in the pluginFileNames() implementation,
preventing loading of real core plugins; change pluginFileNames() (and the
duplicate implementation further down) to either return broader
platform-specific name patterns like "qtmesh_ps1core*" or replace the hardcoded
list with a small discovery routine that uses QDir/QDir::entryList name filters
(e.g., "qtmesh_ps1core*.dll"/"qtmesh_ps1core*.dylib"/"qtmesh_ps1core*.so") to
enumerate all matching files in the plugin directory so real core plugins are
found in addition to the stub.
- Line 69: The current loader returns immediately whatever plugin->createCore()
yields, so if createCore() returns nullptr the loader exits without trying other
candidates; change the logic in EmuCoreLoader.cpp where plugin->createCore() is
invoked to call and store the result in a variable (e.g., auto core =
plugin->createCore()), check if core != nullptr and return it only then,
otherwise log/ignore that plugin and continue iterating over remaining plugins;
after the loop, return nullptr (or produce the existing error path) to indicate
no plugin produced a valid core.

In `@src/PS1/runtime/EmuFramebuffer.h`:
- Line 15: The isValid() check can overflow because width*height*3 is computed
in signed int; change it to compute the expected byte count in an unsigned size
type and compare to rgb24.size(). Specifically, in isValid() compute expected as
size_t(width) * size_t(height) * 3u (and keep the existing width > 0 && height >
0 checks) and then compare rgb24.size() == expected; update the function
signature/implementation of isValid() to use that computed size to avoid signed
overflow when validating rgb24.

In `@src/PS1/runtime/EmuViewport.cpp`:
- Around line 16-20: The code returns early on a null input, leaving m_frame
unchanged and showing stale content; change the flow so m_frame is cleared when
frame.isNull() and the view is updated: assign m_frame = frame (or explicitly
clear m_frame) even when frame.isNull(), then call update() so the "No frame"
state is rendered instead of keeping the previous frame; adjust the block around
frame.isNull(), m_frame and update() (referencing m_frame, frame.isNull(), and
update()) to implement this.

In `@src/PS1/runtime/PS1RipLegalityDialog.cpp`:
- Around line 61-62: Replace the custom breadcrumb category used in the
user-acknowledgement call with the mandated UI action category: update the
SentryReporter::addBreadcrumb call in PS1RipLegalityDialog (the invocation that
currently uses "ps1.rip.dialog.acknowledged") to use "ui.action" as the category
while preserving the existing message ("user acknowledged legality notice") so
the breadcrumb conforms to the required taxonomy for user-facing actions.
- Around line 57-65: The accept path should be blocked unless the acknowledgment
checkbox is checked: in PS1RipLegalityDialog::accept(), only call
setAcknowledged(...) and QDialog::accept() when m_ackCheck->isChecked(); refrain
from calling QDialog::accept() (and optionally setAcknowledged(false) or show a
warning) when the checkbox is unchecked. Update the method so
SentryReporter::addBreadcrumb and QDialog::accept() are executed inside the
m_ackCheck->isChecked() branch and do not close the dialog otherwise.

In `@src/PS1/runtime/PS1RipManager_test.cpp`:
- Line 69: The test currently uses a tautological assertion
EXPECT_GE(errorSpy.count(), 0) which always passes; update the assertion to
verify an actual error was emitted by replacing it with a meaningful check such
as EXPECT_GT(errorSpy.count(), 0) (or EXPECT_EQ(errorSpy.count(),
<expected_count>) if you know the exact number) so the test fails when no errors
are produced; target the assertion that calls errorSpy.count() in
PS1RipManager_test.cpp.
- Around line 105-108: The skip branch after ASSERT_TRUE(startedSpy.wait(3000))
is unreachable; change the flow to first capture the wait result (call
startedSpy.wait(3000) into a bool), then if the wait failed and
startedSpy.empty() && !errorSpy.empty() call GTEST_SKIP(), and finally assert
the wait succeeded (ASSERT_TRUE on the captured bool). This uses
startedSpy.wait, startedSpy.empty, errorSpy.empty and preserves the ASSERT_TRUE
semantics while allowing the conditional GTEST_SKIP path to execute.

In `@src/PS1/runtime/PS1RipManager.cpp`:
- Line 107: The breadcrumb at SentryReporter::addBreadcrumb currently sends the
full absolute path (m_biosPath), which leaks local file paths; change it to send
only the filename by extracting QFileInfo(m_biosPath).fileName() (or equivalent)
and set the category to the mandated I/O category "file.import"; do the same fix
for the other call referenced (line 124) so both SentryReporter::addBreadcrumb
invocations use the sanitized filename and category "file.import" instead of the
absolute path.
- Around line 45-57: The pause state is being cleared in the emulationStarted
and emulationStopped handlers (m_paused = false) but no signal is emitted to
notify listeners; update the lambda connected to PS1RipWorker::emulationStarted
and the one connected to PS1RipWorker::emulationStopped to emit
pausedChanged(false) immediately after clearing m_paused so UI listeners can
update; reference the connect calls for PS1RipWorker::emulationStarted and
PS1RipWorker::emulationStopped and the m_paused member and pausedChanged signal
when making the change.

In `@src/PS1/runtime/PS1RipSessionWindow.cpp`:
- Around line 38-52: Add Sentry breadcrumbs in each toolbar/action handler: in
pickBios and pickIso emit a breadcrumb with category "file.import" describing
the chosen file/action, and in onStart, onStop, onPause, onStep, and onReset
emit breadcrumbs with category "ui.action" describing the control invoked;
locate these methods in PS1RipSessionWindow (pickBios, pickIso, onStart, onStop,
onPause, onStep, onReset) and call the project's Sentry breadcrumb API (or
Sentry::addBreadcrumb equivalent) at the start of each handler with a short
message and appropriate category/level to match the coding guideline.
🪄 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: 4f75a981-01ab-4a2e-87de-24459a22eb70

📥 Commits

Reviewing files that changed from the base of the PR and between 185c2dd and e6239ca.

📒 Files selected for processing (31)
  • CLAUDE.md
  • CMakeLists.txt
  • plugins/ps1core_stub/CMakeLists.txt
  • plugins/ps1core_stub/StubEmuCore.cpp
  • plugins/ps1core_stub/StubEmuCore.h
  • plugins/ps1core_stub/StubEmuCorePlugin.cpp
  • plugins/ps1core_stub/StubEmuCorePlugin.h
  • plugins/ps1core_stub/ps1core_stub.json
  • src/CMakeLists.txt
  • src/PS1/CMakeLists.txt
  • src/PS1/PS1_RIP_DESIGN.md
  • src/PS1/runtime/EmuCore.h
  • src/PS1/runtime/EmuCoreLoader.cpp
  • src/PS1/runtime/EmuCoreLoader.h
  • src/PS1/runtime/EmuCoreLoader_test.cpp
  • src/PS1/runtime/EmuFramebuffer.h
  • src/PS1/runtime/EmuHooks.h
  • src/PS1/runtime/EmuViewport.cpp
  • src/PS1/runtime/EmuViewport.h
  • src/PS1/runtime/IEmuCorePlugin.h
  • src/PS1/runtime/PS1RipLegalityDialog.cpp
  • src/PS1/runtime/PS1RipLegalityDialog.h
  • src/PS1/runtime/PS1RipManager.cpp
  • src/PS1/runtime/PS1RipManager.h
  • src/PS1/runtime/PS1RipManager_test.cpp
  • src/PS1/runtime/PS1RipSessionWindow.cpp
  • src/PS1/runtime/PS1RipSessionWindow.h
  • src/PS1/runtime/PS1RipWorker.cpp
  • src/PS1/runtime/PS1RipWorker.h
  • src/mainwindow.cpp
  • tests/CMakeLists.txt

Comment on lines +53 to +55
m_frameIndex = 0;
fillTestPattern(m_buffers[m_writeIndex]);
m_readIndex = m_writeIndex;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset leaves stale phase state in the first post-reset frame.

Line [54] regenerates pixels from buf.frameIndex, but reset() only clears m_frameIndex. If the selected write buffer was previously used, the displayed reset frame can start from an old phase.

Suggested fix
 void StubEmuCore::reset()
 {
     m_frameIndex = 0;
-    fillTestPattern(m_buffers[m_writeIndex]);
+    EmuFramebuffer &buf = m_buffers[static_cast<size_t>(m_writeIndex)];
+    buf.frameIndex = m_frameIndex;
+    fillTestPattern(buf);
     m_readIndex = m_writeIndex;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
m_frameIndex = 0;
fillTestPattern(m_buffers[m_writeIndex]);
m_readIndex = m_writeIndex;
m_frameIndex = 0;
EmuFramebuffer &buf = m_buffers[static_cast<size_t>(m_writeIndex)];
buf.frameIndex = m_frameIndex;
fillTestPattern(buf);
m_readIndex = m_writeIndex;
🤖 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 `@plugins/ps1core_stub/StubEmuCore.cpp` around lines 53 - 55, The reset leaves
stale per-buffer phase because fillTestPattern reads buf.frameIndex while
reset() only resets m_frameIndex; fix by initializing the selected write
buffer's frame state before regenerating pixels: set the write buffer's
frameIndex (or equivalent phase/state in m_buffers[m_writeIndex]) to
m_frameIndex (or 0) prior to calling fillTestPattern, and then set m_readIndex =
m_writeIndex so the displayed frame uses the new phase; update
StubEmuCore::reset() to adjust m_buffers[m_writeIndex]'s phase field (or call a
helper to sync buffer state) so no old phase remains.

Comment on lines +49 to +51
if (!found) {
GTEST_SKIP() << "PS1 stub core plugin not built beside test binary";
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t skip this prerequisite; fail loudly when the plugin is missing.

Skipping here can mask CI/build-output regressions in plugin installation. This should be an assertion failure.

Proposed fix
-    if (!found) {
-        GTEST_SKIP() << "PS1 stub core plugin not built beside test binary";
-    }
+    ASSERT_TRUE(found) << "PS1 stub core plugin not built beside test binary";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!found) {
GTEST_SKIP() << "PS1 stub core plugin not built beside test binary";
}
ASSERT_TRUE(found) << "PS1 stub core plugin not built beside test binary";
🤖 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/EmuCoreLoader_test.cpp` around lines 49 - 51, Replace the
runtime skip with a hard test failure so missing plugin fails CI: in
EmuCoreLoader_test.cpp where the code currently does "if (!found) { GTEST_SKIP()
<< ...; }", change it to assert the prerequisite (e.g., use ASSERT_TRUE(found)
<< "PS1 stub core plugin not built beside test binary"; or alternatively FAIL()
<< "PS1 stub core plugin not built beside test binary";) so the test fails
loudly when the plugin is missing.

Comment on lines +11 to +14
QString bundleOrAppDir()
{
return QCoreApplication::applicationDirPath();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use bundle-root path resolution on macOS for PS1Cores.

applicationDirPath() points to .../Contents/MacOS in app bundles, not the bundle root expected by the PR contract.

As per coding guidelines "Use macBundlePath() to get .app bundle root on macOS; Ogre resource paths in resources.cfg resolve relative to bundle root, not Contents/MacOS/".

Also applies to: 31-37

🤖 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/EmuCoreLoader.cpp` around lines 11 - 14, bundleOrAppDir()
currently returns QCoreApplication::applicationDirPath(), which points to
Contents/MacOS inside .app bundles; change it to return the bundle root on macOS
by using QCoreApplication::applicationDirPath() fallback to
QCoreApplication::applicationDirPath() only on non-macOS and use
QCoreApplication::applicationDirPath() replacement
QCoreApplication::macBundlePath() under a Q_OS_MAC (or `#ifdef` Q_OS_MAC) guard so
PS1Cores and any code referencing bundleOrAppDir() (also update the similar
logic around the other occurrence at the 31-37 region) resolve resources
relative to the .app bundle root as required by resources.cfg.

Comment on lines +16 to +26
QStringList pluginFileNames()
{
#if defined(Q_OS_WIN)
return {QStringLiteral("qtmesh_ps1core_stub.dll")};
#elif defined(Q_OS_MACOS)
return {QStringLiteral("libqtmesh_ps1core_stub.dylib"),
QStringLiteral("qtmesh_ps1core_stub.dylib")};
#else
return {QStringLiteral("libqtmesh_ps1core_stub.so"),
QStringLiteral("qtmesh_ps1core_stub.so")};
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Loader discovery is hardcoded to the stub plugin name.

This only loads qtmesh_ps1core_stub* and prevents discovering any real core plugin with a different filename.

Proposed fix
-QStringList pluginFileNames()
-{
-#if defined(Q_OS_WIN)
-    return {QStringLiteral("qtmesh_ps1core_stub.dll")};
-#elif defined(Q_OS_MACOS)
-    return {QStringLiteral("libqtmesh_ps1core_stub.dylib"),
-            QStringLiteral("qtmesh_ps1core_stub.dylib")};
-#else
-    return {QStringLiteral("libqtmesh_ps1core_stub.so"),
-            QStringLiteral("qtmesh_ps1core_stub.so")};
-#endif
-}
+QStringList pluginFileNames(const QDir &dir)
+{
+    QStringList out;
+    const QFileInfoList entries = dir.entryInfoList(QDir::Files);
+    for (const QFileInfo &fi : entries) {
+        if (QLibrary::isLibrary(fi.fileName()))
+            out << fi.fileName();
+    }
+    return out;
+}
...
-    const QStringList names = pluginFileNames();
     for (const QString &dirPath : coreSearchPaths()) {
         const QDir dir(dirPath);
         if (!dir.exists())
             continue;
 
-        for (const QString &fileName : names) {
+        for (const QString &fileName : pluginFileNames(dir)) {
             const QString pluginPath = dir.filePath(fileName);

Also applies to: 43-51

🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 22-22: There is an unknown macro here somewhere. Configuration is required. If Q_DECLARE_INTERFACE is a macro then please configure it.

(unknownMacro)

🤖 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/EmuCoreLoader.cpp` around lines 16 - 26, The plugin discovery
currently hardcodes only the "qtmesh_ps1core_stub" filenames in the
pluginFileNames() implementation, preventing loading of real core plugins;
change pluginFileNames() (and the duplicate implementation further down) to
either return broader platform-specific name patterns like "qtmesh_ps1core*" or
replace the hardcoded list with a small discovery routine that uses
QDir/QDir::entryList name filters (e.g.,
"qtmesh_ps1core*.dll"/"qtmesh_ps1core*.dylib"/"qtmesh_ps1core*.so") to enumerate
all matching files in the plugin directory so real core plugins are found in
addition to the stub.

continue;
}

return plugin->createCore();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle null createCore() and keep searching candidates.

If a plugin loads but returns nullptr, the loader exits early instead of trying other plugins and returning a useful error.

Proposed fix
-            return plugin->createCore();
+            if (std::unique_ptr<EmuCore> core = plugin->createCore())
+                return core;
+            if (errorOut)
+                *errorOut = QObject::tr("Plugin loaded but failed to create core: %1").arg(pluginPath);
+            continue;
🤖 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/EmuCoreLoader.cpp` at line 69, The current loader returns
immediately whatever plugin->createCore() yields, so if createCore() returns
nullptr the loader exits without trying other candidates; change the logic in
EmuCoreLoader.cpp where plugin->createCore() is invoked to call and store the
result in a variable (e.g., auto core = plugin->createCore()), check if core !=
nullptr and return it only then, otherwise log/ignore that plugin and continue
iterating over remaining plugins; after the loop, return nullptr (or produce the
existing error path) to indicate no plugin produced a valid core.

EXPECT_FALSE(manager->start());
EXPECT_TRUE(manager->isoPath().isEmpty());
EXPECT_GE(errorSpy.count(), 1);
EXPECT_GE(errorSpy.count(), 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the tautological signal assertion.

Line 69 always passes, so it doesn’t validate error emission. Assert a real expectation instead.

Proposed fix
-    EXPECT_GE(errorSpy.count(), 0);
+    EXPECT_GT(errorSpy.count(), 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EXPECT_GE(errorSpy.count(), 0);
EXPECT_GT(errorSpy.count(), 0);
🤖 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/PS1RipManager_test.cpp` at line 69, The test currently uses a
tautological assertion EXPECT_GE(errorSpy.count(), 0) which always passes;
update the assertion to verify an actual error was emitted by replacing it with
a meaningful check such as EXPECT_GT(errorSpy.count(), 0) (or
EXPECT_EQ(errorSpy.count(), <expected_count>) if you know the exact number) so
the test fails when no errors are produced; target the assertion that calls
errorSpy.count() in PS1RipManager_test.cpp.

Comment on lines +105 to +108
ASSERT_TRUE(startedSpy.wait(3000));
if (startedSpy.empty() && !errorSpy.empty())
GTEST_SKIP() << "Emulator failed to start in test environment";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix unreachable skip logic in startup wait path.

After ASSERT_TRUE(startedSpy.wait(3000)), the skip branch can’t execute. This currently hard-fails in environments where startup times out.

Proposed fix
-    ASSERT_TRUE(startedSpy.wait(3000));
-    if (startedSpy.empty() && !errorSpy.empty())
-        GTEST_SKIP() << "Emulator failed to start in test environment";
+    if (!startedSpy.wait(3000)) {
+        if (!errorSpy.empty())
+            GTEST_SKIP() << "Emulator failed to start in test environment";
+        FAIL() << "Session did not start within timeout";
+    }
🤖 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/PS1RipManager_test.cpp` around lines 105 - 108, The skip
branch after ASSERT_TRUE(startedSpy.wait(3000)) is unreachable; change the flow
to first capture the wait result (call startedSpy.wait(3000) into a bool), then
if the wait failed and startedSpy.empty() && !errorSpy.empty() call
GTEST_SKIP(), and finally assert the wait succeeded (ASSERT_TRUE on the captured
bool). This uses startedSpy.wait, startedSpy.empty, errorSpy.empty and preserves
the ASSERT_TRUE semantics while allowing the conditional GTEST_SKIP path to
execute.

Comment on lines +45 to +57
connect(m_worker, &PS1RipWorker::emulationStarted, this, [this]() {
m_startPending = false;
m_sessionActive = true;
m_paused = false;
emit sessionStarted();
});
connect(m_worker, &PS1RipWorker::emulationStopped, this, [this]() {
m_startPending = false;
m_sessionActive = false;
m_paused = false;
m_captureArmed = false;
emit sessionStopped();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit pausedChanged(false) when start/stop clears pause state.

m_paused is reset on start/stop, but listeners are never notified. That can leave pause UI state stale.

Proposed fix
     connect(m_worker, &PS1RipWorker::emulationStarted, this, [this]() {
+        const bool wasPaused = m_paused;
         m_startPending = false;
         m_sessionActive = true;
         m_paused = false;
+        if (wasPaused)
+            emit pausedChanged(false);
         emit sessionStarted();
     });
     connect(m_worker, &PS1RipWorker::emulationStopped, this, [this]() {
+        const bool wasPaused = m_paused;
         m_startPending = false;
         m_sessionActive = false;
         m_paused = false;
         m_captureArmed = false;
+        if (wasPaused)
+            emit pausedChanged(false);
         emit sessionStopped();
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
connect(m_worker, &PS1RipWorker::emulationStarted, this, [this]() {
m_startPending = false;
m_sessionActive = true;
m_paused = false;
emit sessionStarted();
});
connect(m_worker, &PS1RipWorker::emulationStopped, this, [this]() {
m_startPending = false;
m_sessionActive = false;
m_paused = false;
m_captureArmed = false;
emit sessionStopped();
});
connect(m_worker, &PS1RipWorker::emulationStarted, this, [this]() {
const bool wasPaused = m_paused;
m_startPending = false;
m_sessionActive = true;
m_paused = false;
if (wasPaused)
emit pausedChanged(false);
emit sessionStarted();
});
connect(m_worker, &PS1RipWorker::emulationStopped, this, [this]() {
const bool wasPaused = m_paused;
m_startPending = false;
m_sessionActive = false;
m_paused = false;
m_captureArmed = false;
if (wasPaused)
emit pausedChanged(false);
emit sessionStopped();
});
🤖 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/PS1RipManager.cpp` around lines 45 - 57, The pause state is
being cleared in the emulationStarted and emulationStopped handlers (m_paused =
false) but no signal is emitted to notify listeners; update the lambda connected
to PS1RipWorker::emulationStarted and the one connected to
PS1RipWorker::emulationStopped to emit pausedChanged(false) immediately after
clearing m_paused so UI listeners can update; reference the connect calls for
PS1RipWorker::emulationStarted and PS1RipWorker::emulationStopped and the
m_paused member and pausedChanged signal when making the change.

stop();

m_biosPath = info.absoluteFilePath();
SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.bios.load"), m_biosPath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid sending absolute local file paths in breadcrumbs.

Line 107 and Line 124 currently send full paths (often including usernames) to telemetry, which is a privacy/compliance risk. Emit sanitized values (e.g., filename) and use the mandated I/O category.

Proposed fix
-    SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.bios.load"), m_biosPath);
+    SentryReporter::addBreadcrumb(QStringLiteral("file.import"),
+                                  QFileInfo(m_biosPath).fileName());

-    SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.iso.load"), m_isoPath);
+    SentryReporter::addBreadcrumb(QStringLiteral("file.import"),
+                                  QFileInfo(m_isoPath).fileName());
As per coding guidelines: "All user-facing actions and significant operations must track Sentry breadcrumbs with SentryReporter::addBreadcrumb() using category 'ui.action' for toolbar/menu clicks, 'ai.tool_call' for MCP tools, or 'file.import'/'file.export' for I/O".

Also applies to: 124-124

🤖 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/PS1RipManager.cpp` at line 107, The breadcrumb at
SentryReporter::addBreadcrumb currently sends the full absolute path
(m_biosPath), which leaks local file paths; change it to send only the filename
by extracting QFileInfo(m_biosPath).fileName() (or equivalent) and set the
category to the mandated I/O category "file.import"; do the same fix for the
other call referenced (line 124) so both SentryReporter::addBreadcrumb
invocations use the sanitized filename and category "file.import" instead of the
absolute path.

Comment on lines +38 to +52
auto *biosAct = toolbar->addAction(tr("Load BIOS…"));
connect(biosAct, &QAction::triggered, this, &PS1RipSessionWindow::pickBios);
auto *isoAct = toolbar->addAction(tr("Load ISO…"));
connect(isoAct, &QAction::triggered, this, &PS1RipSessionWindow::pickIso);
toolbar->addSeparator();
auto *startAct = toolbar->addAction(tr("Start"));
connect(startAct, &QAction::triggered, this, &PS1RipSessionWindow::onStart);
auto *stopAct = toolbar->addAction(tr("Stop"));
connect(stopAct, &QAction::triggered, this, &PS1RipSessionWindow::onStop);
auto *pauseAct = toolbar->addAction(tr("Pause"));
connect(pauseAct, &QAction::triggered, this, &PS1RipSessionWindow::onPause);
auto *stepAct = toolbar->addAction(tr("Step"));
connect(stepAct, &QAction::triggered, this, &PS1RipSessionWindow::onStep);
auto *resetAct = toolbar->addAction(tr("Reset"));
connect(resetAct, &QAction::triggered, this, &PS1RipSessionWindow::onReset);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add Sentry breadcrumbs for every toolbar/file action using required categories.

Session controls and BIOS/ISO picks are user-facing actions, but only window open/close currently emit breadcrumbs, and not with the required category convention. Please emit breadcrumbs in each handler (pickBios, pickIso, onStart, onStop, onPause, onStep, onReset) with ui.action / file.import categories.

Suggested patch
@@
-    SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.viewport.open"),
-                                QStringLiteral("PS1 rip session window opened"));
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 rip session window opened"));
@@
-    SentryReporter::addBreadcrumb(QStringLiteral("ps1.rip.viewport.close"),
-                                QStringLiteral("PS1 rip session window closed"));
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 rip session window closed"));
@@
 void PS1RipSessionWindow::pickBios()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("file.import"),
+                                  QStringLiteral("PS1 BIOS picker opened"));
@@
 void PS1RipSessionWindow::pickIso()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("file.import"),
+                                  QStringLiteral("PS1 ISO picker opened"));
@@
 void PS1RipSessionWindow::onStart()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 session start clicked"));
     m_manager->start();
 }
@@
 void PS1RipSessionWindow::onStop()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 session stop clicked"));
     m_manager->stop();
 }
@@
 void PS1RipSessionWindow::onPause()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 session pause clicked"));
     m_manager->pause();
 }
@@
 void PS1RipSessionWindow::onStep()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 session step clicked"));
     m_manager->step();
 }
@@
 void PS1RipSessionWindow::onReset()
 {
+    SentryReporter::addBreadcrumb(QStringLiteral("ui.action"),
+                                  QStringLiteral("PS1 session reset clicked"));
     if (!m_manager->isSessionActive() && !m_manager->isStartPending()) {

As per coding guidelines: “All user-facing actions and significant operations must track Sentry breadcrumbs with category ui.action … or file.import/file.export for I/O.”

Also applies to: 75-76, 83-84, 107-170

🤖 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 38 - 52, Add Sentry
breadcrumbs in each toolbar/action handler: in pickBios and pickIso emit a
breadcrumb with category "file.import" describing the chosen file/action, and in
onStart, onStop, onPause, onStep, and onReset emit breadcrumbs with category
"ui.action" describing the control invoked; locate these methods in
PS1RipSessionWindow (pickBios, pickIso, onStart, onStop, onPause, onStep,
onReset) and call the project's Sentry breadcrumb API (or Sentry::addBreadcrumb
equivalent) at the start of each handler with a short message and appropriate
category/level to match the coding guideline.

@fernandotonon fernandotonon merged commit 3f88aac into master May 17, 2026
2 checks passed
@fernandotonon fernandotonon deleted the feat/ps1-emucore-phase1 branch May 17, 2026 20:04
@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: Embed emulator core via abstract EmuCore interface

1 participant