From 3436e221dec1fffbaa835a9107efa01ae79f3406 Mon Sep 17 00:00:00 2001 From: harry Date: Sun, 14 Jan 2024 08:58:10 -0500 Subject: [PATCH] For Qt GUI, added initial framework for use of the Qt built-in ECMAScript (javascript based) engine. This scripting capability will allow users to load their own custom UI windows (created using Qt Creator tool) in the GUI. This is intended to serve as a the Qt GUI's functional replacement for building GUI elements using IUP in LUA scripts. This is a work in progress. --- src/CMakeLists.txt | 82 +++- src/drivers/Qt/ConsoleWindow.cpp | 80 +++- src/drivers/Qt/ConsoleWindow.h | 2 + src/drivers/Qt/FrameTimingStats.cpp | 18 + src/drivers/Qt/FrameTimingStats.h | 1 + src/drivers/Qt/QtScriptManager.cpp | 653 ++++++++++++++++++++++++++++ src/drivers/Qt/QtScriptManager.h | 144 ++++++ src/drivers/Qt/config.cpp | 1 + src/drivers/Qt/sdl-throttle.cpp | 39 ++ src/drivers/Qt/throttle.h | 9 + 10 files changed, 994 insertions(+), 35 deletions(-) create mode 100644 src/drivers/Qt/QtScriptManager.cpp create mode 100644 src/drivers/Qt/QtScriptManager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0437eaff..908b102a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,34 +10,80 @@ if (${PUBLIC_RELEASE}) endif() if ( ${QT6} ) - message( STATUS "GUI Frontend: Qt6") - set( Qt Qt6 ) -else() - message( STATUS "GUI Frontend: Qt5") - set( Qt Qt5 ) + set( QT 6 ) endif() -if ( ${QHELP} ) - set(QtHelpModule Help) - add_definitions( -D_USE_QHELP ) +if (NOT DEFINED QT) + message( STATUS "Attempting to determine Qt Version...") + find_package( Qt6 COMPONENTS Core) + + if (${Qt6Core_FOUND}) + message( STATUS "Found Qt Version: ${Qt6Core_VERSION}") + set( QT 6 ) + else() + find_package( Qt5 COMPONENTS Core) + + if (${Qt5Core_FOUND}) + message( STATUS "Found Qt Version: ${Qt5Core_VERSION}") + set( QT 5 ) + endif() + endif() endif() if ( ${FCEU_PROFILER_ENABLE} ) message( STATUS "FCEU Profiler Enabled") add_definitions( -D__FCEU_PROFILER_ENABLE__ ) endif() - -if ( ${QT6} ) - find_package( Qt6 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets ${QtHelpModule}) - add_definitions( ${Qt6Widgets_DEFINITIONS} ${Qt6Help_DEFINITIONS} ${Qt6OpenGLWidgets_DEFINITIONS} ) - include_directories( ${Qt6Widgets_INCLUDE_DIRS} ${Qt6Help_INCLUDE_DIRS} ${Qt6OpenGLWidgets_INCLUDE_DIRS} ) +if ( ${QT} EQUAL 6 ) + message( STATUS "GUI Frontend: Qt6") + set( Qt Qt6 ) + find_package( Qt6 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets) + find_package( Qt6 COMPONENTS Help QUIET) + find_package( Qt6 COMPONENTS Qml) + add_definitions( ${Qt6Widgets_DEFINITIONS} ${Qt6Qml_DEFINITIONS} ${Qt6Help_DEFINITIONS} ${Qt6OpenGLWidgets_DEFINITIONS} ) + include_directories( ${Qt6Widgets_INCLUDE_DIRS} ${Qt6Qml_INCLUDE_DIRS} ${Qt6Help_INCLUDE_DIRS} ${Qt6OpenGLWidgets_INCLUDE_DIRS} ) + + if (${Qt6Help_FOUND}) + message( STATUS "Qt6 Help Module Found") + if (${QHELP}) + add_definitions( -D_USE_QHELP ) + endif() + else() + message( STATUS "Qt6 Help Module Not Found") + endif() + + if (${Qt6Qml_FOUND}) + message( STATUS "Qt6 Qml Module Found") + #add_definitions( -D__FCEU_QSCRIPT_ENABLE__ ) + else() + message( STATUS "Qt6 Qml Module Not Found") + endif() else() - find_package( Qt5 REQUIRED COMPONENTS Widgets OpenGL ${QtHelpModule}) - add_definitions( ${Qt5Widgets_DEFINITIONS} ${Qt5Help_DEFINITIONS} ) - include_directories( ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Help_INCLUDE_DIRS} ) -endif() + message( STATUS "GUI Frontend: Qt5") + set( Qt Qt5 ) + find_package( Qt5 REQUIRED COMPONENTS Widgets OpenGL) + find_package( Qt5 COMPONENTS Help QUIET) + find_package( Qt5 COMPONENTS Qml) + add_definitions( ${Qt5Widgets_DEFINITIONS} ${Qt5Qml_DEFINITIONS} ${Qt5Help_DEFINITIONS} ) + include_directories( ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Qml_INCLUDE_DIRS} ${Qt5Help_INCLUDE_DIRS} ) + if (${Qt5Help_FOUND}) + message( STATUS "Qt5 Help Module Found") + if (${QHELP}) + add_definitions( -D_USE_QHELP ) + endif() + else() + message( STATUS "Qt5 Help Module Not Found") + endif() + + if (${Qt5Qml_FOUND}) + message( STATUS "Qt5 Qml Module Found") + add_definitions( -D__FCEU_QSCRIPT_ENABLE__ ) + else() + message( STATUS "Qt5 Qml Module Not Found") + endif() +endif() if(WIN32) find_package(OpenGL REQUIRED) @@ -579,6 +625,7 @@ set(SRC_DRIVERS_SDL ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/ConsoleSoundConf.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/StateRecorderConf.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/iNesHeaderEditor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/QtScriptManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/SplashScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/TraceLogger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/drivers/Qt/AboutWindow.cpp @@ -666,6 +713,7 @@ target_link_libraries( ${APP_NAME} ${ASAN_LDFLAGS} ${GPROF_LDFLAGS} ${${Qt}Widgets_LIBRARIES} ${${Qt}Help_LIBRARIES} + ${${Qt}Qml_LIBRARIES} ${${Qt}OpenGL_LIBRARIES} ${${Qt}OpenGLWidgets_LIBRARIES} ${OPENGL_LDFLAGS} diff --git a/src/drivers/Qt/ConsoleWindow.cpp b/src/drivers/Qt/ConsoleWindow.cpp index 2b4ee2ec..b7030da2 100644 --- a/src/drivers/Qt/ConsoleWindow.cpp +++ b/src/drivers/Qt/ConsoleWindow.cpp @@ -67,6 +67,7 @@ #include "Qt/main.h" #include "Qt/dface.h" #include "Qt/input.h" +#include "Qt/throttle.h" #include "Qt/ColorMenu.h" #include "Qt/ConsoleWindow.h" #include "Qt/InputConf.h" @@ -86,6 +87,7 @@ #include "Qt/TimingConf.h" #include "Qt/FrameTimingStats.h" #include "Qt/LuaControl.h" +#include "Qt/QtScriptManager.h" #include "Qt/CheatsConf.h" #include "Qt/GameGenie.h" #include "Qt/HexEditor.h" @@ -270,6 +272,9 @@ consoleWin_t::consoleWin_t(QWidget *parent) // Create AVI Recording Disk Thread aviDiskThread = new AviRecordDiskThread_t(this); +#ifdef __FCEU_QSCRIPT_ENABLE__ + QtScriptManager::create(this); +#endif scrHandlerConnected = false; } @@ -1079,7 +1084,7 @@ void consoleWin_t::createMainMenu(void) connect( Hotkeys[ HK_SELECT_STATE_NEXT ].getShortcut(), SIGNAL(activated()), this, SLOT(incrementState(void)) ); #ifdef _S9XLUA_H - // File -> Quick Save + // File -> Load Lua loadLuaAct = new QAction(tr("Load &Lua Script"), this); //loadLuaAct->setShortcut( QKeySequence(tr("F5"))); loadLuaAct->setStatusTip(tr("Load Lua Script")); @@ -1090,7 +1095,20 @@ void consoleWin_t::createMainMenu(void) fileMenu->addSeparator(); #else - loadLuaAct = NULL; + loadLuaAct = nullptr; +#endif + +#ifdef _S9XLUA_H + // File -> Load QScript + loadJsAct = new QAction(tr("Load &Qt Script"), this); + loadJsAct->setStatusTip(tr("Load Qt Script")); + connect(loadJsAct, SIGNAL(triggered()), this, SLOT(loadJs(void)) ); + + fileMenu->addAction(loadJsAct); + + fileMenu->addSeparator(); +#else + loadJsAct = NULL; #endif // File -> Screenshot @@ -2981,6 +2999,19 @@ void consoleWin_t::loadLua(void) #endif } +void consoleWin_t::loadJs(void) +{ +#ifdef __FCEU_QSCRIPT_ENABLE__ + QScriptDialog_t *jsCtrlWin; + + //printf("Open JS Control Window\n"); + + jsCtrlWin = new QScriptDialog_t(this); + + jsCtrlWin->show(); +#endif +} + void consoleWin_t::openInputConfWin(void) { //printf("Open Input Config Window\n"); @@ -4546,21 +4577,30 @@ void consoleWin_t::emuFrameFinish(void) { static bool eventProcessingInProg = false; - if ( eventProcessingInProg ) - { // Prevent recursion as processEvents function can double back on us - return; - } - eventProcessingInProg = true; - // Process all events before attempting to render viewport - QCoreApplication::processEvents(); + guiSignalRecvMark(); - eventProcessingInProg = false; + //if ( eventProcessingInProg ) + //{ // Prevent recursion as processEvents function can double back on us + // return; + //} + // Prevent recursion as processEvents function can double back on us + if ( !eventProcessingInProg ) + { + eventProcessingInProg = true; + // Process all events before attempting to render viewport + QCoreApplication::processEvents(); + eventProcessingInProg = false; + } // Update Input Devices FCEUD_UpdateInput(); //printf("EMU Frame Finish\n"); +#ifdef __FCEU_QSCRIPT_ENABLE__ + QtScriptManager::getInstance()->frameFinishedUpdate(); +#endif + transferVideoBuffer(); } @@ -4569,15 +4609,18 @@ void consoleWin_t::updatePeriodic(void) FCEU_PROFILE_FUNC(prof, "updatePeriodic"); static bool eventProcessingInProg = false; - if ( eventProcessingInProg ) - { // Prevent recursion as processEvents function can double back on us - return; + //if ( eventProcessingInProg ) + //{ // Prevent recursion as processEvents function can double back on us + // return; + //} + // Prevent recursion as processEvents function can double back on us + if ( !eventProcessingInProg ) + { + eventProcessingInProg = true; + // Process all events before attempting to render viewport + QCoreApplication::processEvents(); + eventProcessingInProg = false; } - eventProcessingInProg = true; - // Process all events before attempting to render viewport - QCoreApplication::processEvents(); - - eventProcessingInProg = false; // Update Input Devices FCEUD_UpdateInput(); @@ -4863,6 +4906,7 @@ void emulatorThread_t::run(void) void emulatorThread_t::signalFrameFinished(void) { + emuSignalSendMark(); emit frameFinished(); } diff --git a/src/drivers/Qt/ConsoleWindow.h b/src/drivers/Qt/ConsoleWindow.h index 1fc69749..d0fe59fe 100644 --- a/src/drivers/Qt/ConsoleWindow.h +++ b/src/drivers/Qt/ConsoleWindow.h @@ -206,6 +206,7 @@ class consoleWin_t : public QMainWindow QAction *quickLoadAct; QAction *quickSaveAct; QAction *loadLuaAct; + QAction *loadJsAct; QAction *scrShotAct; QAction *quitAct; QAction *inputConfig; @@ -372,6 +373,7 @@ class consoleWin_t : public QMainWindow void incrementState(void); void decrementState(void); void loadLua(void); + void loadJs(void); void takeScreenShot(void); void prepareScreenShot(void); void powerConsoleCB(void); diff --git a/src/drivers/Qt/FrameTimingStats.cpp b/src/drivers/Qt/FrameTimingStats.cpp index 35878850..e2551da8 100644 --- a/src/drivers/Qt/FrameTimingStats.cpp +++ b/src/drivers/Qt/FrameTimingStats.cpp @@ -89,6 +89,7 @@ FrameTimingDialog_t::FrameTimingDialog_t(QWidget *parent) frameTimeIdlePct = new QTreeWidgetItem(); frameLateCount = new QTreeWidgetItem(); videoTimeAbs = new QTreeWidgetItem(); + emuSignalDelay = new QTreeWidgetItem(); tree->addTopLevelItem(frameTimeAbs); tree->addTopLevelItem(frameTimeDel); @@ -97,6 +98,7 @@ FrameTimingDialog_t::FrameTimingDialog_t(QWidget *parent) tree->addTopLevelItem(frameTimeWorkPct); tree->addTopLevelItem(frameTimeIdlePct); tree->addTopLevelItem(videoTimeAbs); + tree->addTopLevelItem(emuSignalDelay); tree->addTopLevelItem(frameLateCount); frameTimeAbs->setFlags(Qt::ItemIsEnabled | Qt::ItemNeverHasChildren); @@ -109,6 +111,7 @@ FrameTimingDialog_t::FrameTimingDialog_t(QWidget *parent) frameTimeWorkPct->setText(0, tr("Frame Work %")); frameTimeIdlePct->setText(0, tr("Frame Idle %")); frameLateCount->setText(0, tr("Frame Late Count")); + emuSignalDelay->setText(0, tr("EMU Signal Delay ms")); videoTimeAbs->setText(0, tr("Video Period ms")); frameTimeAbs->setTextAlignment(0, Qt::AlignLeft); @@ -119,6 +122,7 @@ FrameTimingDialog_t::FrameTimingDialog_t(QWidget *parent) frameTimeIdlePct->setTextAlignment(0, Qt::AlignLeft); frameLateCount->setTextAlignment(0, Qt::AlignLeft); videoTimeAbs->setTextAlignment(0, Qt::AlignLeft); + emuSignalDelay->setTextAlignment(0, Qt::AlignLeft); for (int i = 0; i < 4; i++) { @@ -130,6 +134,7 @@ FrameTimingDialog_t::FrameTimingDialog_t(QWidget *parent) frameTimeIdlePct->setTextAlignment(i + 1, Qt::AlignCenter); frameLateCount->setTextAlignment(i + 1, Qt::AlignCenter); videoTimeAbs->setTextAlignment(i + 1, Qt::AlignCenter); + emuSignalDelay->setTextAlignment(i + 1, Qt::AlignCenter); } hbox = new QHBoxLayout(); @@ -294,6 +299,19 @@ void FrameTimingDialog_t::updateTimingStats(void) sprintf(stmp, "%.3f", stats.videoTimeDel.max * 1e3); videoTimeAbs->setText(4, tr(stmp)); + // Emulator to GUI Thread Signal Delay + sprintf(stmp, "%.3f", stats.emuSignalDelay.tgt * 1e3); + emuSignalDelay->setText(1, tr(stmp)); + + sprintf(stmp, "%.3f", stats.emuSignalDelay.cur * 1e3); + emuSignalDelay->setText(2, tr(stmp)); + + sprintf(stmp, "%.3f", stats.emuSignalDelay.min * 1e3); + emuSignalDelay->setText(3, tr(stmp)); + + sprintf(stmp, "%.3f", stats.emuSignalDelay.max * 1e3); + emuSignalDelay->setText(4, tr(stmp)); + // Late Count sprintf(stmp, "%u", stats.lateCount); frameLateCount->setText(1, tr("0")); diff --git a/src/drivers/Qt/FrameTimingStats.h b/src/drivers/Qt/FrameTimingStats.h index 4131430e..79cae941 100644 --- a/src/drivers/Qt/FrameTimingStats.h +++ b/src/drivers/Qt/FrameTimingStats.h @@ -40,6 +40,7 @@ protected: QTreeWidgetItem *frameTimeIdlePct; QTreeWidgetItem *frameLateCount; QTreeWidgetItem *videoTimeAbs; + QTreeWidgetItem *emuSignalDelay; QGroupBox *statFrame; QTreeWidget *tree; diff --git a/src/drivers/Qt/QtScriptManager.cpp b/src/drivers/Qt/QtScriptManager.cpp new file mode 100644 index 00000000..888edcea --- /dev/null +++ b/src/drivers/Qt/QtScriptManager.cpp @@ -0,0 +1,653 @@ +/* FCE Ultra - NES/Famicom Emulator + * + * Copyright notice for this file: + * Copyright (C) 2020 thor + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +// QtScriptManager.cpp +// +#ifdef __FCEU_QSCRIPT_ENABLE__ +#include +#include +#include + +#ifdef WIN32 +#include +#endif + +#include +#include +#include +#include +#include + +#include "../../fceu.h" +#include "../../movie.h" + +#include "common/os_utils.h" + +#include "Qt/QtScriptManager.h" +#include "Qt/main.h" +#include "Qt/input.h" +#include "Qt/config.h" +#include "Qt/keyscan.h" +#include "Qt/fceuWrapper.h" +#include "Qt/ConsoleUtilities.h" +#include "Qt/ConsoleWindow.h" + +//---------------------------------------------------- +//---- EMU Script Object +//---------------------------------------------------- +EmuScriptObject::EmuScriptObject(QObject* parent) + : QObject(parent) +{ +} +//---------------------------------------------------- +EmuScriptObject::~EmuScriptObject() +{ +} +//---------------------------------------------------- +void EmuScriptObject::print(const QString& msg) +{ + if (dialog != nullptr) + { + dialog->logOutput(msg); + } +} +//---------------------------------------------------- +void EmuScriptObject::softreset() +{ + fceuWrapperSoftReset(); +} +//---------------------------------------------------- +void EmuScriptObject::pause() +{ + FCEUI_SetEmulationPaused( EMULATIONPAUSED_PAUSED ); +} +//---------------------------------------------------- +void EmuScriptObject::unpause() +{ + FCEUI_SetEmulationPaused(0); +} +//---------------------------------------------------- +//---- Qt Script Instance +//---------------------------------------------------- +QtScriptInstance::QtScriptInstance(QObject* parent) + : QObject(parent) +{ + QScriptDialog_t* win = qobject_cast(parent); + + emu = new EmuScriptObject(this); + + if (win != nullptr) + { + dialog = win; + emu->setDialog(dialog); + } + engine = new QJSEngine(this); + + configEngine(); + + QtScriptManager::getInstance()->addScriptInstance(this); +} +//---------------------------------------------------- +QtScriptInstance::~QtScriptInstance() +{ + if (engine != nullptr) + { + engine->deleteLater(); + engine = nullptr; + } + QtScriptManager::getInstance()->removeScriptInstance(this); + + //printf("QtScriptInstance Destroyed\n"); +} +//---------------------------------------------------- +void QtScriptInstance::resetEngine() +{ + if (engine != nullptr) + { + engine->deleteLater(); + engine = nullptr; + } + engine = new QJSEngine(this); + + configEngine(); +} +//---------------------------------------------------- +int QtScriptInstance::configEngine() +{ + engine->installExtensions(QJSEngine::ConsoleExtension); + + QJSValue emuObject = engine->newQObject(emu); + + engine->globalObject().setProperty("emu", emuObject); + + onFrameFinishCallback = QJSValue(); + + return 0; +} +//---------------------------------------------------- +int QtScriptInstance::loadScriptFile( QString filepath ) +{ + QFile scriptFile(filepath); + + if (!scriptFile.open(QIODevice::ReadOnly)) + { + return -1; + } + QTextStream stream(&scriptFile); + QString fileText = stream.readAll(); + scriptFile.close(); + + FCEU_WRAPPER_LOCK(); + QJSValue evalResult = engine->evaluate(fileText, filepath); + FCEU_WRAPPER_UNLOCK(); + + if (evalResult.isError()) + { + print(evalResult.toString()); + return -1; + } + else + { + //printf("Script Evaluation Success!\n"); + } + onFrameFinishCallback = engine->globalObject().property("onFrameFinish"); + + return 0; +} +//---------------------------------------------------- +void QtScriptInstance::print(const QString& msg) +{ + if (dialog) + { + dialog->logOutput(msg); + } +} +//---------------------------------------------------- +void QtScriptInstance::printSymbols(QJSValue& val, int iter) +{ + int i=0; + if (iter > 10) + { + return; + } + QJSValueIterator it(val); + while (it.hasNext()) + { + it.next(); + QJSValue child = it.value(); + qDebug() << iter << ":" << i << " " << it.name() << ": " << child.toString(); + + bool isPrototype = it.name() == "prototype"; + + if (!isPrototype) + { + printSymbols(child, iter + 1); + } + i++; + } +} +//---------------------------------------------------- +int QtScriptInstance::call(const QString& funcName, const QJSValueList& args) +{ + if (engine == nullptr) + { + return -1; + } + if (!engine->globalObject().hasProperty(funcName)) + { + print(QString("No function exists: ") + funcName); + return -1; + } + QJSValue func = engine->globalObject().property(funcName); + + FCEU_WRAPPER_LOCK(); + QJSValue callResult = func.call(args); + FCEU_WRAPPER_UNLOCK(); + + if (callResult.isError()) + { + print(callResult.toString()); + } + else + { + //printf("Script Call Success!\n"); + } + + QJSValue global = engine->globalObject(); + + printSymbols( global ); + + return 0; +} +//---------------------------------------------------- +void QtScriptInstance::onFrameFinish() +{ + if (onFrameFinishCallback.isCallable()) + { + onFrameFinishCallback.call(); + } +} +//---------------------------------------------------- +bool QtScriptInstance::isRunning() +{ + return false; +} +//---------------------------------------------------- +//---- Qt Script Manager +//---------------------------------------------------- +QtScriptManager* QtScriptManager::_instance = nullptr; + +QtScriptManager::QtScriptManager(QObject* parent) + : QObject(parent) +{ + _instance = this; +} +//---------------------------------------------------- +QtScriptManager::~QtScriptManager() +{ + _instance = nullptr; + //printf("QtScriptManager destroyed\n"); +} +//---------------------------------------------------- +QtScriptManager* QtScriptManager::create(QObject* parent) +{ + QtScriptManager* mgr = new QtScriptManager(parent); + + //printf("QtScriptManager created\n"); + + return mgr; +} +//---------------------------------------------------- +void QtScriptManager::addScriptInstance(QtScriptInstance* script) +{ + scriptList.push_back(script); +} +//---------------------------------------------------- +void QtScriptManager::removeScriptInstance(QtScriptInstance* script) +{ + auto it = scriptList.begin(); + + while (it != scriptList.end()) + { + if (*it == script) + { + it = scriptList.erase(it); + } + else + { + it++; + } + } +} +//---------------------------------------------------- +void QtScriptManager::frameFinishedUpdate() +{ + FCEU_WRAPPER_LOCK(); + for (auto script : scriptList) + { + script->onFrameFinish(); + } + FCEU_WRAPPER_UNLOCK(); +} +//---------------------------------------------------- +//---- Qt Script Dialog Window +//---------------------------------------------------- +QScriptDialog_t::QScriptDialog_t(QWidget *parent) + : QDialog(parent, Qt::Window) +{ + QVBoxLayout *mainLayout; + QHBoxLayout *hbox; + QPushButton *closeButton; + QLabel *lbl; + std::string filename; + QSettings settings; + + resize(512, 512); + + setWindowTitle(tr("Qt Java Script Control")); + + mainLayout = new QVBoxLayout(); + + lbl = new QLabel(tr("Script File:")); + + scriptPath = new QLineEdit(); + scriptArgs = new QLineEdit(); + + g_config->getOption("SDL.LastLoadJs", &filename); + + scriptPath->setText( tr(filename.c_str()) ); + scriptPath->setClearButtonEnabled(true); + scriptArgs->setClearButtonEnabled(true); + + jsOutput = new QTextEdit(); + jsOutput->setReadOnly(true); + + hbox = new QHBoxLayout(); + + browseButton = new QPushButton(tr("Browse")); + stopButton = new QPushButton(tr("Stop")); + + scriptInstance = new QtScriptInstance(this); + + if (scriptInstance->isRunning()) + { + startButton = new QPushButton(tr("Restart")); + } + else + { + startButton = new QPushButton(tr("Start")); + } + + stopButton->setEnabled(scriptInstance->isRunning()); + + connect(browseButton, SIGNAL(clicked()), this, SLOT(openScriptFile(void))); + connect(stopButton, SIGNAL(clicked()), this, SLOT(stopScript(void))); + connect(startButton, SIGNAL(clicked()), this, SLOT(startScript(void))); + + hbox->addWidget(browseButton); + hbox->addWidget(stopButton); + hbox->addWidget(startButton); + + mainLayout->addWidget(lbl); + mainLayout->addWidget(scriptPath); + mainLayout->addLayout(hbox); + + hbox = new QHBoxLayout(); + lbl = new QLabel(tr("Arguments:")); + + hbox->addWidget(lbl); + hbox->addWidget(scriptArgs); + + mainLayout->addLayout(hbox); + + lbl = new QLabel(tr("Output Console:")); + mainLayout->addWidget(lbl); + mainLayout->addWidget(jsOutput); + + closeButton = new QPushButton( tr("Close") ); + closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton)); + connect(closeButton, SIGNAL(clicked(void)), this, SLOT(closeWindow(void))); + + hbox = new QHBoxLayout(); + hbox->addStretch(5); + hbox->addWidget( closeButton, 1 ); + mainLayout->addLayout( hbox ); + + setLayout(mainLayout); + + //winList.push_back(this); + + periodicTimer = new QTimer(this); + + connect(periodicTimer, &QTimer::timeout, this, &QScriptDialog_t::updatePeriodic); + + periodicTimer->start(200); // 5hz + + restoreGeometry(settings.value("QScriptWindow/geometry").toByteArray()); +} + +//---------------------------------------------------- +QScriptDialog_t::~QScriptDialog_t(void) +{ + QSettings settings; + std::list::iterator it; + + //printf("Destroy JS Control Window\n"); + + periodicTimer->stop(); + + //for (it = winList.begin(); it != winList.end(); it++) + //{ + // if ((*it) == this) + // { + // winList.erase(it); + // //printf("Removing JS Window\n"); + // break; + // } + //} + settings.setValue("QScriptWindow/geometry", saveGeometry()); +} +//---------------------------------------------------- +void QScriptDialog_t::closeEvent(QCloseEvent *event) +{ + //printf("JS Control Close Window Event\n"); + done(0); + deleteLater(); + event->accept(); +} +//---------------------------------------------------- +void QScriptDialog_t::closeWindow(void) +{ + //printf("JS Control Close Window\n"); + done(0); + deleteLater(); +} +//---------------------------------------------------- +void QScriptDialog_t::updatePeriodic(void) +{ + // TODO + //printf("Update JS\n"); + //if (updateJSDisplay) + //{ + // updateJSWindows(); + // updateJSDisplay = false; + //} +} +//---------------------------------------------------- +void QScriptDialog_t::openJSKillMessageBox(void) +{ + int ret; + QMessageBox msgBox(this); + + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText(tr("The JS script running has been running a long time.\nIt may have gone crazy. Kill it? (I won't ask again if you say No)\n")); + msgBox.setStandardButtons(QMessageBox::Yes); + msgBox.addButton(QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + + ret = msgBox.exec(); + + if (ret == QMessageBox::Yes) + { + } +} +//---------------------------------------------------- +void QScriptDialog_t::openScriptFile(void) +{ + int ret, useNativeFileDialogVal; + QString filename; + std::string last; + std::string dir; + const char *exePath = nullptr; + const char *jsPath = nullptr; + QFileDialog dialog(this, tr("Open JS Script")); + QList urls; + QDir d; + + exePath = fceuExecutablePath(); + + //urls = dialog.sidebarUrls(); + urls << QUrl::fromLocalFile(QDir::rootPath()); + urls << QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first()); + urls << QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).first()); + urls << QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::DownloadLocation).first()); + urls << QUrl::fromLocalFile(QDir(FCEUI_GetBaseDirectory()).absolutePath()); + + if (exePath[0] != 0) + { + d.setPath(QString(exePath) + "/../jsScripts"); + + if (d.exists()) + { + urls << QUrl::fromLocalFile(d.absolutePath()); + } + } +#ifndef WIN32 + d.setPath("/usr/share/fceux/jsScripts"); + + if (d.exists()) + { + urls << QUrl::fromLocalFile(d.absolutePath()); + } +#endif + + jsPath = getenv("FCEU_QSCRIPT_PATH"); + + // Parse LUA_PATH and add to urls + if (jsPath) + { + int i, j; + char stmp[2048]; + + i = j = 0; + while (jsPath[i] != 0) + { + if (jsPath[i] == ';') + { + stmp[j] = 0; + + if (j > 0) + { + d.setPath(stmp); + + if (d.exists()) + { + urls << QUrl::fromLocalFile(d.absolutePath()); + } + } + j = 0; + } + else + { + stmp[j] = jsPath[i]; + j++; + } + i++; + } + + stmp[j] = 0; + + if (j > 0) + { + d.setPath(stmp); + + if (d.exists()) + { + urls << QUrl::fromLocalFile(d.absolutePath()); + } + } + } + + dialog.setFileMode(QFileDialog::ExistingFile); + + dialog.setNameFilter(tr("JS Scripts (*.js *.JS) ;; All files (*)")); + + dialog.setViewMode(QFileDialog::List); + dialog.setFilter(QDir::AllEntries | QDir::AllDirs | QDir::Hidden); + dialog.setLabelText(QFileDialog::Accept, tr("Load")); + + g_config->getOption("SDL.LastLoadJs", &last); + + if (last.size() == 0) + { +#ifdef WIN32 + last.assign(FCEUI_GetBaseDirectory()); +#else + last.assign("/usr/share/fceux/jsScripts"); +#endif + } + + getDirFromFile(last.c_str(), dir); + + dialog.setDirectory(tr(dir.c_str())); + + // Check config option to use native file dialog or not + g_config->getOption("SDL.UseNativeFileDialog", &useNativeFileDialogVal); + + dialog.setOption(QFileDialog::DontUseNativeDialog, !useNativeFileDialogVal); + dialog.setSidebarUrls(urls); + + ret = dialog.exec(); + + if (ret) + { + QStringList fileList; + fileList = dialog.selectedFiles(); + + if (fileList.size() > 0) + { + filename = fileList[0]; + } + } + + if (filename.isNull()) + { + return; + } + qDebug() << "selected file path : " << filename.toUtf8(); + + g_config->setOption("SDL.LastLoadJs", filename.toStdString().c_str()); + + scriptPath->setText(filename); + +} +//---------------------------------------------------- +void QScriptDialog_t::startScript(void) +{ + scriptInstance->resetEngine(); + if (scriptInstance->loadScriptFile(scriptPath->text())) + { + // Script parsing error + return; + } + // TODO add option to pass options to script main. + QJSValue argArray = scriptInstance->getEngine()->newArray(4); + argArray.setProperty(0, "arg1"); + argArray.setProperty(1, "arg2"); + argArray.setProperty(2, "arg3"); + + QJSValueList argList = { argArray }; + + scriptInstance->call("main", argList); +} +//---------------------------------------------------- +void QScriptDialog_t::stopScript(void) +{ +} +//---------------------------------------------------- +void QScriptDialog_t::refreshState(void) +{ + if (scriptInstance->isRunning()) + { + stopButton->setEnabled(true); + startButton->setText(tr("Restart")); + } + else + { + stopButton->setEnabled(false); + startButton->setText(tr("Start")); + } +} +//---------------------------------------------------- +void QScriptDialog_t::logOutput(const QString& text) +{ + jsOutput->insertPlainText(text); +} +//---------------------------------------------------- +#endif // __FCEU_QSCRIPT_ENABLE__ diff --git a/src/drivers/Qt/QtScriptManager.h b/src/drivers/Qt/QtScriptManager.h new file mode 100644 index 00000000..2844fcb3 --- /dev/null +++ b/src/drivers/Qt/QtScriptManager.h @@ -0,0 +1,144 @@ +// QtScriptManager.h +// + +#pragma once + +#ifdef __FCEU_QSCRIPT_ENABLE__ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Qt/main.h" +#include "utils/timeStamp.h" + +class QScriptDialog_t; + +class EmuScriptObject: public QObject +{ + Q_OBJECT +public: + EmuScriptObject(QObject* parent = nullptr); + ~EmuScriptObject(); + + void setDialog(QScriptDialog_t* _dialog){ dialog = _dialog; } + +private: + QScriptDialog_t* dialog = nullptr; + +public slots: + Q_INVOKABLE void print(const QString& msg); + Q_INVOKABLE void softreset(); + Q_INVOKABLE void pause(); + Q_INVOKABLE void unpause(); + +}; + +class QtScriptInstance : public QObject +{ + Q_OBJECT +public: + QtScriptInstance(QObject* parent = nullptr); + ~QtScriptInstance(); + + void resetEngine(); + int loadScriptFile(QString filepath); + + bool isRunning(); + + int call(const QString& funcName, const QJSValueList& args = QJSValueList()); + void onFrameFinish(); + + QJSEngine* getEngine(){ return engine; }; +private: + + int configEngine(); + void printSymbols(QJSValue& val, int iter = 0); + + QJSEngine* engine = nullptr; + QScriptDialog_t* dialog = nullptr; + EmuScriptObject* emu = nullptr; + QJSValue onFrameFinishCallback; + +public slots: + Q_INVOKABLE void print(const QString& msg); +}; + +class QtScriptManager : public QObject +{ + Q_OBJECT + +public: + QtScriptManager(QObject* parent = nullptr); + ~QtScriptManager(); + + static QtScriptManager* getInstance(){ return _instance; } + static QtScriptManager* create(QObject* parent = nullptr); + + void addScriptInstance(QtScriptInstance* script); + void removeScriptInstance(QtScriptInstance* script); +private: + static QtScriptManager* _instance; + + QList scriptList; + FCEU::timeStampRecord lastFrameUpdate; + +public slots: + void frameFinishedUpdate(); +}; + +class QScriptDialog_t : public QDialog +{ + Q_OBJECT + +public: + QScriptDialog_t(QWidget *parent = nullptr); + ~QScriptDialog_t(void); + + void refreshState(void); + void logOutput(const QString& text); + +protected: + void closeEvent(QCloseEvent *bar); + void openJSKillMessageBox(void); + + QTimer *periodicTimer; + QLineEdit *scriptPath; + QLineEdit *scriptArgs; + QPushButton *browseButton; + QPushButton *stopButton; + QPushButton *startButton; + QTextEdit *jsOutput; + QtScriptInstance *scriptInstance; + +private: +public slots: + void closeWindow(void); +private slots: + void updatePeriodic(void); + void openScriptFile(void); + void startScript(void); + void stopScript(void); +}; + +// Formatted print +//int LuaPrintfToWindowConsole( __FCEU_PRINTF_FORMAT const char *format, ...) __FCEU_PRINTF_ATTRIBUTE( 1, 2 ); + +//void PrintToWindowConsole(intptr_t hDlgAsInt, const char *str); + +//int LuaKillMessageBox(void); + +#endif // __FCEU_QSCRIPT_ENABLE__ diff --git a/src/drivers/Qt/config.cpp b/src/drivers/Qt/config.cpp index ded54a16..50442ffb 100644 --- a/src/drivers/Qt/config.cpp +++ b/src/drivers/Qt/config.cpp @@ -801,6 +801,7 @@ InitConfig() config->addOption("_lastsavestateas", "SDL.LastSaveStateAs", savPath ); config->addOption("_lastopenmovie", "SDL.LastOpenMovie", movPath); config->addOption("_lastloadlua", "SDL.LastLoadLua", ""); + config->addOption("_lastloadjs", "SDL.LastLoadJs", ""); config->addOption("SDL.HelpFilePath", ""); config->addOption("SDL.AviFilePath", ""); config->addOption("SDL.WavFilePath", ""); diff --git a/src/drivers/Qt/sdl-throttle.cpp b/src/drivers/Qt/sdl-throttle.cpp index 800ad121..8d5daece 100644 --- a/src/drivers/Qt/sdl-throttle.cpp +++ b/src/drivers/Qt/sdl-throttle.cpp @@ -39,6 +39,7 @@ static const double Normal = 1.0; // 1x speed (around 60 fps on NTSC) static uint32 frameLateCounter = 0; static FCEU::timeStampRecord Lasttime, Nexttime, Latetime; static FCEU::timeStampRecord DesiredFrameTime, HalfFrameTime, QuarterFrameTime, DoubleFrameTime; +static FCEU::timeStampRecord emuSignalTx, guiSignalRx, emuSignalLatency; static double desired_frametime = (1.0 / 60.099823); static double desired_frameRate = (60.099823); static double baseframeRate = (60.099823); @@ -52,6 +53,9 @@ static double videoLastTs = 0.0; static double videoPeriodCur = 0.0; static double videoPeriodMin = 1.0; static double videoPeriodMax = 0.0; +static double emuLatencyCur = 0.0; +static double emuLatencyMin = 1.0; +static double emuLatencyMax = 0.0; static bool keepFrameTimeStats = false; static int InFrame = 0; double g_fpsScale = Normal; // used by sdl.cpp @@ -193,6 +197,11 @@ int getFrameTimingStats( struct frameTimingStat_t *stats ) stats->videoTimeDel.min = videoPeriodMin; stats->videoTimeDel.max = videoPeriodMax; + stats->emuSignalDelay.tgt = 0.0; + stats->emuSignalDelay.cur = emuLatencyCur; + stats->emuSignalDelay.min = emuLatencyMin; + stats->emuSignalDelay.max = emuLatencyMax; + return 0; } @@ -216,6 +225,34 @@ void videoBufferSwapMark(void) } } +void emuSignalSendMark(void) +{ + if ( keepFrameTimeStats ) + { + emuSignalTx.readNew(); + } +} + +void guiSignalRecvMark(void) +{ + if ( keepFrameTimeStats ) + { + guiSignalRx.readNew(); + emuSignalLatency = guiSignalRx - emuSignalTx; + + emuLatencyCur = emuSignalLatency.toSeconds(); + + if ( emuLatencyCur < emuLatencyMin ) + { + emuLatencyMin = emuLatencyCur; + } + if ( emuLatencyCur > emuLatencyMax ) + { + emuLatencyMax = emuLatencyCur; + } + } +} + void resetFrameTiming(void) { frameLateCounter = 0; @@ -225,6 +262,8 @@ void resetFrameTiming(void) frameIdleMin = 1.0; videoPeriodMin = 1.0; videoPeriodMax = 0.0; + emuLatencyMin = 1.0; + emuLatencyMax = 0.0; } /* LOGMUL = exp(log(2) / 3) diff --git a/src/drivers/Qt/throttle.h b/src/drivers/Qt/throttle.h index 7cffdbe6..64f4baa4 100644 --- a/src/drivers/Qt/throttle.h +++ b/src/drivers/Qt/throttle.h @@ -43,6 +43,13 @@ struct frameTimingStat_t double max; } videoTimeDel; + struct { + double tgt; + double cur; + double min; + double max; + } emuSignalDelay; + unsigned int lateCount; bool enabled; @@ -52,6 +59,8 @@ void resetFrameTiming(void); void setFrameTimingEnable( bool enable ); int getFrameTimingStats( struct frameTimingStat_t *stats ); void videoBufferSwapMark(void); +void emuSignalSendMark(void); +void guiSignalRecvMark(void); double getHighPrecTimeStamp(void); double getFrameRate(void); double getFrameRateAdjustmentRatio(void);