diff --git a/.github/workflows/qt-wasm.yml b/.github/workflows/qt-wasm.yml new file mode 100644 index 00000000..bc7b13ab --- /dev/null +++ b/.github/workflows/qt-wasm.yml @@ -0,0 +1,60 @@ +name: Build for Qt for WebAssembly + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install cached emscripten + id: cache + uses: actions/cache@v3 + with: + path: 'emsdk' + key: 'emsdk-1.39.8' + + - name: Install emscripten + if: steps.cache.outputs.cache-hit != 'true' + run: | + curl -L https://github.com/emscripten-core/emsdk/archive/HEAD.tar.gz | tar xz + mv emsdk-* emsdk + ./emsdk/emsdk install 1.39.8 + ./emsdk/emsdk activate 1.39.8 + + - name: Install Qt + uses: jurplel/install-qt-action@43ec12788e42f375acfcb2cec059edfb9572fbaa # v3 + with: + version: '5.15.2' + host: 'linux' + target: 'desktop' + arch: 'wasm_32' + cache: true + + - name: Build firebird + run: | + . ./emsdk/emsdk_env.sh + mkdir build + cd build + qmake .. + make -j4 + + - name: Upload zip + uses: actions/upload-artifact@v3 + with: + name: firebird-emu-wasm + path: | + build/*.html + build/*.js + build/*.wasm + build/*.svg + if-no-files-found: error diff --git a/core/emu.h b/core/emu.h index 2e10ae4d..9f356c4b 100644 --- a/core/emu.h +++ b/core/emu.h @@ -15,7 +15,7 @@ extern "C" { #endif // Can also be set manually -#if !defined(__i386__) && !defined(__x86_64__) && !(defined(__arm__) && !defined(__thumb__)) && !(defined(__aarch64__)) +#if !defined(__i386__) && !defined(__x86_64__) && !(defined(__arm__) && !defined(__thumb__)) && !(defined(__aarch64__)) && !defined(NO_TRANSLATION) #define NO_TRANSLATION #endif diff --git a/core/os/os-emscripten.c b/core/os/os-emscripten.c index 2da300cf..e1509e6b 100644 --- a/core/os/os-emscripten.c +++ b/core/os/os-emscripten.c @@ -3,6 +3,7 @@ #include #include #include +#include #include diff --git a/emscripten/Makefile b/emscripten/Makefile index 878fed4f..78056fe4 100644 --- a/emscripten/Makefile +++ b/emscripten/Makefile @@ -13,7 +13,7 @@ CSOURCES := ../core/armsnippets_loader.c ../core/asmcode.c ../core/casplus.c CPPSOURCES := ../core/arm_interpreter.cpp ../core/coproc.cpp ../core/cpu.cpp ../core/debug.cpp ../core/emu.cpp \ ../core/flash.cpp ../core/gif.cpp ../core/thumb_interpreter.cpp ../core/usblink_queue.cpp main.cpp \ - ../core/fieldparser.cpp + ../core/fieldparser.cpp ../core/cx2.cpp ../core/usb_cx2.cpp ../core/usblink_cx2.cpp OBJS = $(patsubst %.c, %.bc, $(CSOURCES)) OBJS += $(patsubst %.cpp, %.bc, $(CPPSOURCES)) @@ -21,10 +21,10 @@ OBJS += $(patsubst %.cpp, %.bc, $(CPPSOURCES)) all: $(OUTPUT).js %.bc: %.c Makefile - $(CC) $(CFLAGS) $< -o $@ + $(CC) $(CFLAGS) -c $< -o $@ %.bc: %.cpp Makefile - $(CXX) $(CXXFLAGS) $< -o $@ + $(CXX) $(CXXFLAGS) -c $< -o $@ test: $(OUTPUT).js emrun --safe_firefox_profile --browser firefox $(OUTPUT).html diff --git a/emscripten/main.cpp b/emscripten/main.cpp index 52188647..aeb8c18e 100644 --- a/emscripten/main.cpp +++ b/emscripten/main.cpp @@ -5,6 +5,9 @@ #include "core/mmu.h" #include "core/debug.h" #include "core/emu.h" +#include "core/lcd.h" +#include "core/schedule.h" +#include "core/mem.h" void gui_do_stuff(bool wait) { diff --git a/emuthread.cpp b/emuthread.cpp index d52ddfe2..c9b75c8d 100644 --- a/emuthread.cpp +++ b/emuthread.cpp @@ -14,8 +14,10 @@ #include #endif -#include "core/debug.h" #include "core/emu.h" +#include "core/mem.h" +#include "core/mmu.h" +#include "core/schedule.h" #include "core/usblink_queue.h" EmuThread emu_thread; @@ -110,45 +112,36 @@ void throttle_timer_wait(unsigned int usec) } EmuThread::EmuThread(QObject *parent) : - QThread(parent) + QTimer(parent) { // There can only be one instance, this global one. assert(&emu_thread == this); // Set default settings debug_on_start = debug_on_warn = false; + + connect(this, &QTimer::timeout, this, &EmuThread::loopStep); + connect(this, &EmuThread::speedChanged, this, [&](double d) { + if(turbo_mode) + setInterval(0); + else + setInterval(std::max(interval() * d, 1.0)); + }); } //Called occasionally, only way to do something in the same thread the emulator runs in. void EmuThread::doStuff(bool wait) { - do - { if(do_suspend) { bool success = emu_suspend(snapshot_path.c_str()); do_suspend = false; emit suspended(success); } - - if(enter_debugger) - { - setPaused(false); - enter_debugger = false; - if(!in_debugger) - debugger(DBG_USER, 0); - } - - if(is_paused && wait) - msleep(100); - - } while(is_paused && wait); } -void EmuThread::run() +void EmuThread::start() { - setTerminationEnabled(); - path_boot1 = QDir::toNativeSeparators(boot1).toStdString(); path_flash = QDir::toNativeSeparators(flash).toStdString(); @@ -162,28 +155,15 @@ void EmuThread::run() do_resume = false; - if(success) - emu_loop(do_reset); - - emit stopped(); + if(success) { + startup(do_reset); + setInterval(1); + QTimer::start(); + } } void EmuThread::throttleTimerWait(unsigned int usec) { - if(usec <= 1) - return; - - #ifdef Q_OS_WINDOWS - // QThread::usleep uses Sleep, which may sleep up to ~32ms more! - // Use nanosleep inside a timeBeginPeriod/timeEndPeriod block for accuracy. - timeBeginPeriod(10); - struct timespec ts{}; - ts.tv_nsec = usec * 1000; - nanosleep(&ts, nullptr); - timeEndPeriod(10); - #else - QThread::usleep(usec); - #endif } void EmuThread::setTurboMode(bool enabled) @@ -208,6 +188,75 @@ void EmuThread::debuggerInput(QString str) debug_callback(debug_input.c_str()); } +void EmuThread::startup(bool reset) +{ + if(reset) + { + memset(mem_areas[1].ptr, 0, mem_areas[1].size); + + memset(&arm, 0, sizeof arm); + arm.control = 0x00050078; + arm.cpsr_low28 = MODE_SVC | 0xC0; + cpu_events &= EVENT_DEBUG_STEP; + + sched_reset(); + sched.items[SCHED_THROTTLE].clock = CLOCK_27M; + extern void throttle_interval_event(int index); + sched.items[SCHED_THROTTLE].proc = throttle_interval_event; + + memory_reset(); + } + + addr_cache_flush(); + + sched_update_next_event(0); + + exiting = false; +} + +void EmuThread::loopStep() +{ + int i = 1000; + while(i--) + { + sched_process_pending_events(); + while (!exiting && cycle_count_delta < 0) + { + if (cpu_events & EVENT_RESET) { + gui_status_printf("Reset"); + startup(true); + break; + } + + if (cpu_events & EVENT_SLEEP) { + assert(emulate_cx2); + cycle_count_delta = 0; + break; + } + + if (cpu_events & (EVENT_FIQ | EVENT_IRQ)) { + // Align PC in case the interrupt occurred immediately after a jump + if (arm.cpsr_low28 & 0x20) + arm.reg[15] &= ~1; + else + arm.reg[15] &= ~3; + + if (cpu_events & EVENT_WAITING) + arm.reg[15] += 4; // Skip over wait instruction + + arm.reg[15] += 4; + cpu_exception((cpu_events & EVENT_FIQ) ? EX_FIQ : EX_IRQ); + } + cpu_events &= ~EVENT_WAITING; + + if (arm.cpsr_low28 & 0x20) + cpu_thumb_loop(); + else + cpu_arm_loop(); + } + } +} + void EmuThread::setPaused(bool paused) { this->is_paused = paused; @@ -216,8 +265,7 @@ void EmuThread::setPaused(bool paused) bool EmuThread::stop() { - if(!isRunning()) - return true; + QTimer::stop(); exiting = true; setPaused(false); @@ -226,13 +274,6 @@ bool EmuThread::stop() // Cause the cpu core to leave the loop and check for events cycle_count_delta = 0; - if(!this->wait(200)) - { - terminate(); - if(!this->wait(200)) - return false; - } - emu_cleanup(); return true; } diff --git a/emuthread.h b/emuthread.h index af33e29d..6d29c898 100644 --- a/emuthread.h +++ b/emuthread.h @@ -1,9 +1,9 @@ #ifndef EMUTHREAD_H #define EMUTHREAD_H -#include +#include -class EmuThread : public QThread +class EmuThread : public QTimer { Q_OBJECT public: @@ -12,6 +12,8 @@ class EmuThread : public QThread void doStuff(bool wait); void throttleTimerWait(unsigned int usec); + bool isRunning() { return isActive(); }; + QString boot1, flash; unsigned int port_gdb = 0, port_rdbg = 0; @@ -39,7 +41,7 @@ class EmuThread : public QThread void debugInputRequested(bool b); public slots: - virtual void run() override; + void start(); // State void setPaused(bool is_paused); @@ -56,7 +58,11 @@ public slots: void enterDebugger(); void debuggerInput(QString str); + void loopStep(); + private: + void startup(bool reset); + bool enter_debugger = false; bool is_paused = false, do_suspend = false, do_resume = false; std::string debug_input, snapshot_path; diff --git a/firebird.pro b/firebird.pro index f4e8c0d5..94b0dd0e 100644 --- a/firebird.pro +++ b/firebird.pro @@ -42,7 +42,11 @@ unix: !android { QMAKE_CFLAGS += -g -std=gnu11 -Wall -Wextra QMAKE_CXXFLAGS += -g -Wall -Wextra -D QT_NO_CAST_FROM_ASCII -LIBS += -lz +wasm { + QMAKE_CFLAGS += -s USE_ZLIB + QMAKE_CXXFLAGS += -s USE_ZLIB + QMAKE_LFLAGS += -s USE_ZLIB +} else: LIBS += -lz # Override bad default options to enable better optimizations QMAKE_CFLAGS_RELEASE = -O3 -DNDEBUG @@ -53,7 +57,9 @@ QMAKE_CXXFLAGS_RELEASE = -O3 -DNDEBUG # with Qt's -reduce-relocations option (QTBUG-86173). # MinGW fails with # lto1.exe: internal compiler error: in gen_subprogram_die, at dwarf2out.c:22668 -!clang | !if(linux|freebsd): !win32: CONFIG += ltcg +# wasm makes qmake misbehave: the various "-s FOO" options get their "-s" merged +# into a single one. +!clang | !if(linux|freebsd|wasm): !win32: CONFIG += ltcg # noexecstack is not supported by MinGW's as !win32 { @@ -79,7 +85,7 @@ android { QMAKE_LFLAGS += -Wl,-Bsymbolic } -ios|android: DEFINES += MOBILE_UI +ios|android|wasm: DEFINES += MOBILE_UI ios { DEFINES += IS_IOS_BUILD diff --git a/qml/ConfigPageKits.qml b/qml/ConfigPageKits.qml index e7d50738..d80dd53c 100644 --- a/qml/ConfigPageKits.qml +++ b/qml/ConfigPageKits.qml @@ -31,13 +31,13 @@ ColumnLayout { GridLayout { anchors.fill: parent - columns: (width < 550 || Qt.platform.os === "android") ? 2 : 4 + columns: (width < 550 || Qt.platform.os === "android") ? 3 : 6 FBLabel { Layout.columnSpan: parent.columns Layout.fillWidth: true color: "red" - visible: boot1Edit.filePath == "" || flashEdit.filePath == "" + visible: kitList.currentItem.myData.boot1 === "" || kitList.currentItem.myData.flash === "" wrapMode: Text.WordWrap text: qsTr("You need to specify files for Boot1 and Flash") } @@ -50,6 +50,7 @@ ColumnLayout { TextField { id: nameEdit placeholderText: qsTr("Name") + Layout.columnSpan: 2 Layout.fillWidth: true text: kitList.currentItem.myData.name @@ -65,15 +66,22 @@ ColumnLayout { elide: Text.ElideMiddle } - FileSelect { - id: boot1Edit + FBLabel { + property string filePath: kitList.currentItem.myData.boot1 + elide: "ElideRight" + Layout.fillWidth: true - filePath: kitList.currentItem.myData.boot1 - onFilePathChanged: { - if(filePath !== kitList.currentItem.myData.boot1) - kitModel.setDataRow(kitList.currentIndex, filePath, KitModel.Boot1Role); - filePath = Qt.binding(function() { return kitList.currentItem.myData.boot1; }); - } + // Allow the label to shrink below its implicitWidth. + // Without this, the layout doesn't allow it to go smaller... + Layout.preferredWidth: 100 + + font.italic: filePath === "" + text: filePath === "" ? qsTr("(none)") : Emu.basename(filePath) + } + + IconButton { + icon: "qrc:/icons/resources/icons/document-edit.png" + onClicked: Emu.loadFile(kitList.currentIndex, KitModel.Boot1Role) } FBLabel { @@ -81,24 +89,22 @@ ColumnLayout { elide: Text.ElideMiddle } - FileSelect { - id: flashEdit + FBLabel { + property string filePath: kitList.currentItem.myData.flash + elide: "ElideRight" + Layout.fillWidth: true - filePath: kitList.currentItem.myData.flash - onFilePathChanged: { - if(filePath !== kitList.currentItem.myData.flash) - kitModel.setDataRow(kitList.currentIndex, filePath, KitModel.FlashRole); - filePath = Qt.binding(function() { return kitList.currentItem.myData.flash; }); - } - showCreateButton: true - onCreate: flashDialog.visible = true + // Allow the label to shrink below its implicitWidth. + // Without this, the layout doesn't allow it to go smaller... + Layout.preferredWidth: 100 + + font.italic: filePath === "" + text: filePath === "" ? qsTr("(none)") : Emu.basename(filePath) } - FlashDialog { - id: flashDialog - onFlashCreated: { - kitModel.setDataRow(kitList.currentIndex, filePath, KitModel.FlashRole); - } + IconButton { + icon: "qrc:/icons/resources/icons/document-edit.png" + onClicked: Emu.loadFile(kitList.currentIndex, KitModel.FlashRole) } FBLabel { @@ -106,16 +112,22 @@ ColumnLayout { elide: Text.ElideMiddle } - FileSelect { - id: snapshotEdit + FBLabel { + property string filePath: kitList.currentItem.myData.snapshot + elide: "ElideRight" + Layout.fillWidth: true - selectExisting: false - filePath: kitList.currentItem.myData.snapshot - onFilePathChanged: { - if(filePath !== kitList.currentItem.myData.snapshot) - kitModel.setDataRow(kitList.currentIndex, filePath, KitModel.SnapshotRole); - filePath = Qt.binding(function() { return kitList.currentItem.myData.snapshot; }); - } + // Allow the label to shrink below its implicitWidth. + // Without this, the layout doesn't allow it to go smaller... + Layout.preferredWidth: 100 + + font.italic: filePath === "" + text: filePath === "" ? qsTr("(none)") : Emu.basename(filePath) + } + + IconButton { + icon: "qrc:/icons/resources/icons/document-edit.png" + onClicked: Emu.loadFile(kitList.currentIndex, KitModel.SnapshotRole) } } } diff --git a/qml/Firebird/UIComponents/qmldir b/qml/Firebird/UIComponents/qmldir index d3353c64..d20e149d 100644 --- a/qml/Firebird/UIComponents/qmldir +++ b/qml/Firebird/UIComponents/qmldir @@ -2,6 +2,7 @@ module Firebird.UIComponents PageDelegate 1.0 PageDelegate.qml PageList 1.0 PageList.qml ConfigPages 1.0 ConfigPages.qml +IconButton 1.0 IconButton.qml KitList 1.0 KitList.qml FBLabel 1.0 FBLabel.qml FileSelect 1.0 FileSelect.qml diff --git a/qml/MobileUIDrawer.qml b/qml/MobileUIDrawer.qml index bde2437c..c0fdc454 100644 --- a/qml/MobileUIDrawer.qml +++ b/qml/MobileUIDrawer.qml @@ -99,6 +99,16 @@ Rectangle { } onClicked: { + // Different behaviour on WASM: Do not save the flash, + // only save snapshots. Create a new file if none configured yet. + if(Qt.platform.os === "wasm") { + if(!Emu.saveSnapshot()) + saveFailedDialog.visible = true; + + closeDrawer(); + return; + } + var flash_path = Emu.getFlashPath(); var snap_path = Emu.getSnapshotPath(); diff --git a/qmlbridge.cpp b/qmlbridge.cpp index f1233f9e..bbd204a5 100644 --- a/qmlbridge.cpp +++ b/qmlbridge.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include "emuthread.h" @@ -409,6 +410,46 @@ QString QMLBridge::osDescription(QString path) return QString::fromStdString(version); } +void QMLBridge::loadFile(int index, int role) +{ + QFileDialog::getOpenFileContent(QStringLiteral("File%1 (*.*)").arg(role), [=](const QString &, const QByteArray &fileContent) { + int kitId = kit_model.getDataRow(index, KitModel::IDRole).toInt(); + QString path = QStringLiteral("/tmp/kit%1-%2").arg(kitId).arg(role); + QFile file(path); + if (file.open(QFile::WriteOnly)) { + file.write(fileContent); + file.close(); + kit_model.setDataRow(index, path, role); + } + }); +} + +bool QMLBridge::saveSnapshot() +{ + const int kitIndex = kit_model.indexForID(current_kit_id); + QString filename = kit_model.getDataRow(kitIndex, KitModel::SnapshotRole).toString(); + // If the kit doesn't have a snapshot file assigned, do it now + if(filename.isEmpty()) { + // Same algorithm as above + filename = QStringLiteral("/tmp/kit%1-%2").arg(current_kit_id).arg(KitModel::SnapshotRole); + kit_model.setDataRow(kitIndex, filename, KitModel::SnapshotRole); + } + + // Write the snapshot to the file + if(!emu_suspend(filename.toUtf8().constData())) + return false; + + // Read in all the data (again) + QFile snapshotFile(filename); + if(!snapshotFile.open(QIODevice::ReadOnly)) + return false; + + auto snapshotData = snapshotFile.readAll(); + QFileDialog::saveFileContent(snapshotData, QStringLiteral("%1-snapshot.img").arg(kit_model.getDataRow(kitIndex, KitModel::NameRole).toString())); + + return true; +} + void QMLBridge::setActive(bool b) { if(is_active == b) diff --git a/qmlbridge.h b/qmlbridge.h index bac8576d..4cb3096f 100644 --- a/qmlbridge.h +++ b/qmlbridge.h @@ -121,6 +121,9 @@ class QMLBridge : public QObject Q_INVOKABLE QString manufDescription(QString path); Q_INVOKABLE QString osDescription(QString path); + Q_INVOKABLE void loadFile(int index, int role); + Q_INVOKABLE bool saveSnapshot(); + Q_INVOKABLE bool saveDialogSupported(); void setActive(bool b); diff --git a/qtframebuffer.cpp b/qtframebuffer.cpp index 8e3ca877..6b08c07a 100644 --- a/qtframebuffer.cpp +++ b/qtframebuffer.cpp @@ -42,9 +42,8 @@ QImage renderFramebuffer() void paintFramebuffer(QPainter *p) { -#ifdef IS_IOS_BUILD +#if defined(Q_OS_IOS) || defined(Q_OS_WASM) // Apparently, this is needed (will be 2 on retina screens) - // TODO: actually make sure Android doesn't need that as well static const double devicePixelRatio = ((QGuiApplication*)QCoreApplication::instance())->primaryScreen()->devicePixelRatio(); #else // Has to be 1 on desktop, even on retina (tested on OS X 10.11 with one retina, one non-retina, and both ; same on Win VM) diff --git a/usblinktreewidget.cpp b/usblinktreewidget.cpp index 14231c24..df9d9f07 100644 --- a/usblinktreewidget.cpp +++ b/usblinktreewidget.cpp @@ -17,8 +17,8 @@ USBLinkTreeWidget::USBLinkTreeWidget(QWidget *parent) connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(customContextMenuRequested(QPoint))); connect(this, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(dataChangedHandler(QTreeWidgetItem*,int))); // This is a Qt::BlockingQueuedConnection as the usblink_dirlist_* family of functions needs to enumerate over the items directly after emitting the signal. - connect(this, SIGNAL(wantToAddTreeItem(QTreeWidgetItem*,QTreeWidgetItem*)), this, SLOT(addTreeItem(QTreeWidgetItem*,QTreeWidgetItem*)), Qt::BlockingQueuedConnection); - connect(this, SIGNAL(wantToReload()), this, SLOT(reloadFilebrowser()), Qt::QueuedConnection); + connect(this, SIGNAL(wantToAddTreeItem(QTreeWidgetItem*,QTreeWidgetItem*)), this, SLOT(addTreeItem(QTreeWidgetItem*,QTreeWidgetItem*))); + connect(this, SIGNAL(wantToReload()), this, SLOT(reloadFilebrowser())); this->setAcceptDrops(true);