diff --git a/ClockReceiver/VSyncPredictor.hpp b/ClockReceiver/VSyncPredictor.hpp index 229a8d636..f34ed55c5 100644 --- a/ClockReceiver/VSyncPredictor.hpp +++ b/ClockReceiver/VSyncPredictor.hpp @@ -63,6 +63,13 @@ class VSyncPredictor { frame_duration_ = Nanos(1'000'000'000.0f / rate); } + /*! + @returns The time this class currently believes a whole frame occupies. + */ + Time::Nanos frame_duration() { + return frame_duration_; + } + /*! Adds a record of how much jitter was experienced in scheduling; these values will be factored into the @c suggested_draw_time if supplied. @@ -87,15 +94,13 @@ class VSyncPredictor { (if those figures are being supplied). */ Nanos suggested_draw_time() { - const auto mean = redraw_period_.mean() - timer_jitter_.mean() - vsync_jitter_.mean(); + const auto mean = redraw_period_.mean() + timer_jitter_.mean() + vsync_jitter_.mean(); const auto variance = redraw_period_.variance() + timer_jitter_.variance() + vsync_jitter_.variance(); // Permit three standard deviations from the mean, to cover 99.9% of cases. - const auto period = mean - Nanos(3.0f * sqrt(float(variance))); + const auto period = mean + Nanos(3.0f * sqrt(float(variance))); - assert(abs(period) < 10'000'000'000); - - return last_vsync_ + period; + return last_vsync_ + frame_duration_ - period; } private: @@ -109,7 +114,6 @@ class VSyncPredictor { } void post(Time::Nanos value) { - assert(abs(value) < 10'000'000'000); // 10 seconds is a very liberal maximum. sum_ -= history_[write_pointer_]; sum_ += value; history_[write_pointer_] = value; diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 3afe5e70e..2d0882f27 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -45,7 +45,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { MainWindow::MainWindow(const QString &fileName) { init(); - launchFile(fileName); + if(!launchFile(fileName)) { + setUIPhase(UIPhase::SelectingMachine); + } } void MainWindow::deleteMachine() { @@ -210,11 +212,17 @@ void MainWindow::insertFile(const QString &fileName) { mediaTarget->insert_media(media); } -void MainWindow::launchFile(const QString &fileName) { +bool MainWindow::launchFile(const QString &fileName) { targets = Analyser::Static::GetTargets(fileName.toStdString()); if(!targets.empty()) { openFileName = QFileInfo(fileName).fileName(); launchMachine(); + return true; + } else { + QMessageBox msgBox; + msgBox.setText("Unable to open file: " + fileName); + msgBox.exec(); + return false; } } @@ -707,6 +715,7 @@ void MainWindow::dropEvent(QDropEvent* event) { bool foundROM = false; const auto appDataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString(); + QString unusedRoms; for(const auto &url: event->mimeData()->urls()) { const char *const name = url.toLocalFile().toUtf8(); FILE *const file = fopen(name, "rb"); @@ -716,6 +725,7 @@ void MainWindow::dropEvent(QDropEvent* event) { CRC::CRC32 generator; const uint32_t crc = generator.compute_crc(*contents); + bool wasUsed = false; for(const auto &rom: missingRoms) { if(std::find(rom.crc32s.begin(), rom.crc32s.end(), crc) != rom.crc32s.end()) { foundROM = true; @@ -731,10 +741,22 @@ void MainWindow::dropEvent(QDropEvent* event) { FILE *const target = fopen(destination.c_str(), "wb"); fwrite(contents->data(), 1, contents->size(), target); fclose(target); + + wasUsed = true; } } + + if(!wasUsed) { + if(!unusedRoms.isEmpty()) unusedRoms += ", "; + unusedRoms += url.fileName(); + } } + if(!unusedRoms.isEmpty()) { + QMessageBox msgBox; + msgBox.setText("Couldn't identify ROMs: " + unusedRoms); + msgBox.exec(); + } if(foundROM) launchMachine(); } break; } @@ -1338,24 +1360,25 @@ void MainWindow::addActivityObserver() { } void MainWindow::register_led(const std::string &name) { + std::lock_guard guard(ledStatusesLock); ledStatuses[name] = false; - updateStatusBarText(); + QMetaObject::invokeMethod(this, "updateStatusBarText"); } void MainWindow::set_led_status(const std::string &name, bool isLit) { + std::lock_guard guard(ledStatusesLock); ledStatuses[name] = isLit; - updateStatusBarText(); // Assumption here: Qt's attempt at automatic thread confinement will work here. + QMetaObject::invokeMethod(this, "updateStatusBarText"); } void MainWindow::updateStatusBarText() { QString fullText; - bool isFirst = true; + std::lock_guard guard(ledStatusesLock); for(const auto &pair: ledStatuses) { - if(!isFirst) fullText += " | "; + if(!fullText.isEmpty()) fullText += " | "; fullText += QString::fromStdString(pair.first); fullText += " "; fullText += pair.second ? "■" : "□"; - isFirst = false; } statusBar()->showMessage(fullText); } diff --git a/OSBindings/Qt/mainwindow.h b/OSBindings/Qt/mainwindow.h index 4bbe4d108..22be7e739 100644 --- a/OSBindings/Qt/mainwindow.h +++ b/OSBindings/Qt/mainwindow.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "audiobuffer.h" @@ -80,6 +81,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat private slots: void startMachine(); + void updateStatusBarText(); private: void start_appleII(); @@ -100,7 +102,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat QAction *insertAction = nullptr; void insertFile(const QString &fileName); - void launchFile(const QString &fileName); + bool launchFile(const QString &fileName); void launchTarget(std::unique_ptr &&); void restoreSelections(); @@ -144,9 +146,11 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat void register_led(const std::string &) override; void set_led_status(const std::string &, bool) override; + + std::recursive_mutex ledStatusesLock; std::map ledStatuses; + void addActivityObserver(); - void updateStatusBarText(); }; #endif // MAINWINDOW_H diff --git a/OSBindings/Qt/scantargetwidget.cpp b/OSBindings/Qt/scantargetwidget.cpp index 99072b486..3fbbc7c53 100644 --- a/OSBindings/Qt/scantargetwidget.cpp +++ b/OSBindings/Qt/scantargetwidget.cpp @@ -86,7 +86,7 @@ void ScanTargetWidget::vsync() { const auto time_now = Time::nanos_now(); requestedRedrawTime = vsyncPredictor.suggested_draw_time(); const auto delay_time = (requestedRedrawTime - time_now) / 1'000'000; - if(delay_time > 0) { + if(delay_time > 0 && delay_time < vsyncPredictor.frame_duration()) { QTimer::singleShot(delay_time, this, SLOT(repaint())); } else { requestedRedrawTime = 0; diff --git a/README.md b/README.md index 9c081df62..8f27e6a83 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Clock Signal Clock Signal ('CLK') is an emulator for tourists that seeks to be invisible. Users directly launch classic software with no emulator or per-emulated-machine learning curve. -[Releases](https://github.com/TomHarte/CLK/releases) are hosted on GitHub. +macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). For desktop Linux it is also available as a [Snap](https://snapcraft.io/clock-signal). On the Mac it is a native Cocoa application; under Linux, BSD and other UNIXes and UNIX-alikes it can be built either with Qt or with SDL; the Qt build should be considered preliminary and is currently closely bound to X11 as Qt doesn't abstract game-like keyboard handling.