-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ps1): Phase 1 EmuCore plugin, viewport, and session UI #577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5333e3b
839bdb8
e6239ca
92a226c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| if(NOT ENABLE_PS1_RIP) | ||
| return() | ||
| endif() | ||
|
|
||
| set(PS1_RUNTIME_INCLUDE "${CMAKE_SOURCE_DIR}/src/PS1/runtime") | ||
|
|
||
| add_library(qtmesh_ps1core_stub MODULE | ||
| StubEmuCore.cpp | ||
| StubEmuCorePlugin.cpp | ||
| ) | ||
|
|
||
| target_include_directories(qtmesh_ps1core_stub PRIVATE | ||
| ${PS1_RUNTIME_INCLUDE} | ||
| ${CMAKE_CURRENT_SOURCE_DIR} | ||
| ) | ||
|
|
||
| target_link_libraries(qtmesh_ps1core_stub PRIVATE Qt6::Core) | ||
|
|
||
| set_target_properties(qtmesh_ps1core_stub PROPERTIES | ||
| PREFIX "" | ||
| LIBRARY_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/PS1Cores" | ||
| RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/PS1Cores" | ||
| ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/PS1Cores" | ||
| ) | ||
|
|
||
| if(BUILD_TESTS AND TARGET UnitTests) | ||
| add_dependencies(UnitTests qtmesh_ps1core_stub) | ||
| endif() | ||
|
|
||
| if(BUILD_QT_MESH_EDITOR AND TARGET ${CMAKE_PROJECT_NAME}) | ||
| add_dependencies(${CMAKE_PROJECT_NAME} qtmesh_ps1core_stub) | ||
| endif() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| #include "StubEmuCore.h" | ||
|
|
||
| #include <QFileInfo> | ||
|
|
||
| StubEmuCore::StubEmuCore() | ||
| { | ||
| for (EmuFramebuffer &buf : m_buffers) { | ||
| buf.width = kWidth; | ||
| buf.height = kHeight; | ||
| buf.rgb24.resize(kWidth * kHeight * 3); | ||
| } | ||
| fillTestPattern(m_buffers[m_writeIndex]); | ||
| } | ||
|
|
||
| QString StubEmuCore::coreId() const | ||
| { | ||
| return QStringLiteral("stub"); | ||
| } | ||
|
|
||
| bool StubEmuCore::loadBios(const QString &biosPath) | ||
| { | ||
| const QFileInfo info(biosPath); | ||
| if (!info.exists() || !info.isFile()) | ||
| return false; | ||
| m_biosPath = info.absoluteFilePath(); | ||
| return true; | ||
| } | ||
|
|
||
| bool StubEmuCore::loadIso(const QString &isoPath) | ||
| { | ||
| const QFileInfo info(isoPath); | ||
| if (!info.exists() || !info.isFile()) | ||
| return false; | ||
| m_isoPath = info.absoluteFilePath(); | ||
| return true; | ||
| } | ||
|
|
||
| void StubEmuCore::runFrame() | ||
| { | ||
| if (m_biosPath.isEmpty() || m_isoPath.isEmpty()) | ||
| return; | ||
|
|
||
| EmuFramebuffer &buf = m_buffers[static_cast<size_t>(m_writeIndex)]; | ||
| buf.frameIndex = ++m_frameIndex; | ||
| fillTestPattern(buf); | ||
|
|
||
| m_readIndex = m_writeIndex; | ||
| m_writeIndex = (m_writeIndex + 1) % 3; | ||
| } | ||
|
|
||
| void StubEmuCore::reset() | ||
| { | ||
| m_frameIndex = 0; | ||
| fillTestPattern(m_buffers[m_writeIndex]); | ||
| m_readIndex = m_writeIndex; | ||
| } | ||
|
|
||
| const EmuFramebuffer &StubEmuCore::framebuffer() const | ||
| { | ||
| return m_buffers[static_cast<size_t>(m_readIndex)]; | ||
| } | ||
|
|
||
| void StubEmuCore::setHooks(EmuHooks *hooks) | ||
| { | ||
| m_hooks = hooks; | ||
| } | ||
|
|
||
| void StubEmuCore::fillTestPattern(EmuFramebuffer &buf) | ||
| { | ||
| const int w = buf.width; | ||
| const int h = buf.height; | ||
| auto *pixels = reinterpret_cast<uchar *>(buf.rgb24.data()); | ||
| const quint8 phase = static_cast<quint8>(buf.frameIndex % 256); | ||
|
|
||
| for (int y = 0; y < h; ++y) { | ||
| for (int x = 0; x < w; ++x) { | ||
| const int i = (y * w + x) * 3; | ||
| pixels[i + 0] = static_cast<uchar>((x + phase) & 0xFF); | ||
| pixels[i + 1] = static_cast<uchar>((y + phase) & 0xFF); | ||
| pixels[i + 2] = static_cast<uchar>(((x ^ y) + phase) & 0xFF); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #ifndef STUBEMUCORE_H | ||
| #define STUBEMUCORE_H | ||
|
|
||
| #include "EmuCore.h" | ||
|
|
||
| #include <QString> | ||
| #include <array> | ||
|
|
||
| /** Reference / CI core: validates BIOS+ISO paths and renders a test pattern (#415). */ | ||
| class StubEmuCore final : public EmuCore | ||
| { | ||
| public: | ||
| StubEmuCore(); | ||
|
|
||
| QString coreId() const override; | ||
| bool loadBios(const QString &biosPath) override; | ||
| bool loadIso(const QString &isoPath) override; | ||
| void runFrame() override; | ||
| void reset() override; | ||
| const EmuFramebuffer &framebuffer() const override; | ||
| void setHooks(EmuHooks *hooks) override; | ||
|
|
||
| private: | ||
| void fillTestPattern(EmuFramebuffer &buf); | ||
|
|
||
| static constexpr int kWidth = 320; | ||
| static constexpr int kHeight = 240; | ||
|
|
||
| QString m_biosPath; | ||
| QString m_isoPath; | ||
| EmuHooks *m_hooks = nullptr; | ||
| quint64 m_frameIndex = 0; | ||
| std::array<EmuFramebuffer, 3> m_buffers{}; | ||
| int m_readIndex = 0; | ||
| int m_writeIndex = 1; | ||
| }; | ||
|
|
||
| #endif // STUBEMUCORE_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #include "StubEmuCorePlugin.h" | ||
| #include "StubEmuCore.h" | ||
|
|
||
| QString StubEmuCorePlugin::pluginId() const | ||
| { | ||
| return QStringLiteral("stub"); | ||
| } | ||
|
|
||
| std::unique_ptr<EmuCore> StubEmuCorePlugin::createCore() | ||
| { | ||
| return std::make_unique<StubEmuCore>(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #ifndef STUBEMUCOREPLUGIN_H | ||
| #define STUBEMUCOREPLUGIN_H | ||
|
|
||
| #include <QObject> | ||
| #include "IEmuCorePlugin.h" | ||
|
|
||
| class StubEmuCorePlugin : public QObject, public IEmuCorePlugin | ||
| { | ||
| Q_OBJECT | ||
| Q_PLUGIN_METADATA(IID IEmuCorePlugin_iid FILE "ps1core_stub.json") | ||
| Q_INTERFACES(IEmuCorePlugin) | ||
|
|
||
| public: | ||
| QString pluginId() const override; | ||
| std::unique_ptr<EmuCore> createCore() override; | ||
| }; | ||
|
|
||
| #endif // STUBEMUCOREPLUGIN_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "Keys": [], | ||
| "MetaData": { | ||
| "description": "QtMeshEditor PS1 stub emulator core (test pattern)" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #ifndef EMUCORE_H | ||
| #define EMUCORE_H | ||
|
|
||
| #include "EmuFramebuffer.h" | ||
| #include "EmuHooks.h" | ||
|
|
||
| #include <QString> | ||
| #include <memory> | ||
|
|
||
| /** | ||
| * Abstract PS1 emulator core (#415). Implemented by dynamically loaded plugins | ||
| * under <app>/PS1Cores/ — never linked into the main QtMeshEditor binary. | ||
| */ | ||
| class EmuCore | ||
| { | ||
| public: | ||
| virtual ~EmuCore() = default; | ||
|
|
||
| virtual QString coreId() const = 0; | ||
| virtual bool loadBios(const QString &biosPath) = 0; | ||
| virtual bool loadIso(const QString &isoPath) = 0; | ||
| virtual void runFrame() = 0; | ||
| virtual void reset() = 0; | ||
| virtual const EmuFramebuffer &framebuffer() const = 0; | ||
| virtual void setHooks(EmuHooks *hooks) = 0; | ||
| }; | ||
|
|
||
| #endif // EMUCORE_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #include "EmuCoreLoader.h" | ||
| #include "IEmuCorePlugin.h" | ||
|
|
||
| #include <QCoreApplication> | ||
| #include <QDir> | ||
| #include <QFileInfo> | ||
| #include <QPluginLoader> | ||
|
|
||
| namespace { | ||
|
|
||
| QString bundleOrAppDir() | ||
| { | ||
| return QCoreApplication::applicationDirPath(); | ||
| } | ||
|
Comment on lines
+11
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use bundle-root path resolution on macOS for
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 |
||
|
|
||
| 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 | ||
|
Comment on lines
+16
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loader discovery is hardcoded to the stub plugin name. This only loads 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 |
||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| QStringList EmuCoreLoader::coreSearchPaths() | ||
| { | ||
| QStringList paths; | ||
| const QString base = bundleOrAppDir(); | ||
| paths << QDir(base).filePath(QStringLiteral("PS1Cores")); | ||
| paths << QDir(QCoreApplication::applicationDirPath()).filePath(QStringLiteral("PS1Cores")); | ||
| paths.removeDuplicates(); | ||
| return paths; | ||
| } | ||
|
|
||
| std::unique_ptr<EmuCore> EmuCoreLoader::loadCore(QString *errorOut) | ||
| { | ||
| const QStringList names = pluginFileNames(); | ||
| for (const QString &dirPath : coreSearchPaths()) { | ||
| const QDir dir(dirPath); | ||
| if (!dir.exists()) | ||
| continue; | ||
|
|
||
| for (const QString &fileName : names) { | ||
| const QString pluginPath = dir.filePath(fileName); | ||
| if (!QFileInfo::exists(pluginPath)) | ||
| continue; | ||
|
|
||
| QPluginLoader loader(pluginPath); | ||
| QObject *instance = loader.instance(); | ||
| if (!instance) { | ||
| if (errorOut) | ||
|
Check failure on line 57 in src/PS1/runtime/EmuCoreLoader.cpp
|
||
| *errorOut = loader.errorString(); | ||
| continue; | ||
| } | ||
|
|
||
| auto *plugin = qobject_cast<IEmuCorePlugin *>(instance); | ||
| if (!plugin) { | ||
| if (errorOut) | ||
|
Check failure on line 64 in src/PS1/runtime/EmuCoreLoader.cpp
|
||
| *errorOut = QObject::tr("Plugin does not implement IEmuCorePlugin: %1").arg(pluginPath); | ||
| continue; | ||
| } | ||
|
|
||
| return plugin->createCore(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle null If a plugin loads but returns 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 |
||
| } | ||
| } | ||
|
|
||
| if (errorOut) { | ||
| *errorOut = QObject::tr( | ||
| "No PS1 emulator core found in PS1Cores/. " | ||
| "Build with ENABLE_PS1_RIP=ON to install the stub core."); | ||
| } | ||
| return nullptr; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #ifndef EMUCORELOADER_H | ||
| #define EMUCORELOADER_H | ||
|
|
||
| #include "EmuCore.h" | ||
|
|
||
| #include <QString> | ||
| #include <memory> | ||
|
|
||
| /** | ||
| * Loads an EmuCore implementation from <app>/PS1Cores/*.so at runtime. | ||
|
Check warning on line 10 in src/PS1/runtime/EmuCoreLoader.h
|
||
| */ | ||
| class EmuCoreLoader | ||
| { | ||
| public: | ||
| static QStringList coreSearchPaths(); | ||
| static std::unique_ptr<EmuCore> loadCore(QString *errorOut = nullptr); | ||
| }; | ||
|
|
||
| #endif // EMUCORELOADER_H | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset leaves stale phase state in the first post-reset frame.
Line [54] regenerates pixels from
buf.frameIndex, butreset()only clearsm_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
🤖 Prompt for AI Agents