From 79833deeaf5ccf4479c1426561e0ad85a237548d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 14 Jun 2020 19:26:56 -0400 Subject: [PATCH] With some attempt at vsync prediction, seeks to smooth audio/video output. There's plenty more work to do here, but hopefully it takes the issue immediately off the table. --- ClockReceiver/VSyncPredictor.hpp | 63 +++++++++++++++++++ .../Clock Signal.xcodeproj/project.pbxproj | 4 +- OSBindings/Qt/ClockSignal.pro | 2 +- OSBindings/Qt/mainwindow.cpp | 3 +- OSBindings/Qt/scantargetwidget.cpp | 19 +++++- OSBindings/Qt/scantargetwidget.h | 7 ++- 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 ClockReceiver/VSyncPredictor.hpp diff --git a/ClockReceiver/VSyncPredictor.hpp b/ClockReceiver/VSyncPredictor.hpp new file mode 100644 index 000000000..4703dea27 --- /dev/null +++ b/ClockReceiver/VSyncPredictor.hpp @@ -0,0 +1,63 @@ +// +// VSyncPredictor.hpp +// Clock Signal +// +// Created by Thomas Harte on 14/06/2020. +// Copyright © 2020 Thomas Harte. All rights reserved. +// + +#ifndef VSyncPredictor_hpp +#define VSyncPredictor_hpp + +#include "TimeTypes.hpp" + +namespace Time { + +/*! + For platforms that provide no avenue into vsync tracking other than block-until-sync, + this class tracks: (i) how long frame draw takes; and (ii) the apparent frame period; in order + to suggest when you should next start drawing. +*/ +class VSyncPredictor { + public: + void begin_redraw() { + redraw_begin_time_ = nanos_now(); + } + + void end_redraw() { + const auto redraw_length = nanos_now() - redraw_begin_time_; + redraw_period_ = + ((redraw_period_ * 9) + + ((redraw_length) * 1)) / 10; + } + + void announce_vsync() { + const auto vsync_time = nanos_now(); + if(last_vsync_) { + // Use an IIR to try to converge on frame times. + vsync_period_ = + ((vsync_period_ * 9) + + ((vsync_time - last_vsync_) * 1)) / 10; + } + last_vsync_ = vsync_time; + } + + Nanos suggested_draw_time() { + // TODO: this is a very simple version of how this calculation + // should be made. It's tracking the average amount of time these + // things take, therefore will often be wrong. Deviations need to + // be accounted for. + return last_vsync_ + vsync_period_ - redraw_period_; + } + + private: + Nanos redraw_begin_time_ = 0; + Nanos last_vsync_ = 0; + + Nanos vsync_period_ = 1'000'000'000 / 60; // Seems like a good first guess. + Nanos redraw_period_ = 0; +}; + +} + +#endif /* VSyncPredictor_hpp */ diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 551416ab2..62bfff491 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1317,6 +1317,7 @@ 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CSROMFetcher.mm; sourceTree = ""; }; 4B98A0601FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSXStaticAnalyserTests.mm; sourceTree = ""; }; 4B98A1CD1FFADEC400ADF63B /* MSX ROMs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "MSX ROMs"; sourceTree = ""; }; + 4B996B2D2496DAC2001660EF /* VSyncPredictor.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = VSyncPredictor.hpp; sourceTree = ""; }; 4B9BE3FE203A0C0600FFAE60 /* MultiSpeaker.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MultiSpeaker.cpp; sourceTree = ""; }; 4B9BE3FF203A0C0600FFAE60 /* MultiSpeaker.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MultiSpeaker.hpp; sourceTree = ""; }; 4B9D0C4A22C7D70900DE1AD3 /* 68000BCDTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000BCDTests.mm; sourceTree = ""; }; @@ -3855,8 +3856,9 @@ 4B8A7E85212F988200F2BBC6 /* DeferredQueue.hpp */, 4BB06B211F316A3F00600C7A /* ForceInline.hpp */, 4B80214322EE7C3E00068002 /* JustInTime.hpp */, - 4B449C942063389900A095C8 /* TimeTypes.hpp */, 4B644ED023F0FB55006C0CC5 /* ScanSynchroniser.hpp */, + 4B449C942063389900A095C8 /* TimeTypes.hpp */, + 4B996B2D2496DAC2001660EF /* VSyncPredictor.hpp */, ); name = ClockReceiver; path = ../../ClockReceiver; diff --git a/OSBindings/Qt/ClockSignal.pro b/OSBindings/Qt/ClockSignal.pro index 8d8ce6204..865d08449 100644 --- a/OSBindings/Qt/ClockSignal.pro +++ b/OSBindings/Qt/ClockSignal.pro @@ -1,4 +1,4 @@ -QT += core gui multimedia +QT += core gui multimedia quickwidgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 89f822d40..042ffc98a 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -47,7 +47,6 @@ void MainWindow::createActions() { openAct->setStatusTip(tr("Open an existing file")); connect(openAct, &QAction::triggered, this, &MainWindow::open); fileMenu->addAction(openAct); - } void MainWindow::open() { @@ -143,7 +142,7 @@ void MainWindow::launchMachine() { // Install audio output if required. const auto audio_producer = machine->audio_producer(); if(audio_producer) { - static constexpr size_t samplesPerBuffer = 256; + static constexpr size_t samplesPerBuffer = 2048; // TODO: select this dynamically; it's very high. const auto speaker = audio_producer->get_speaker(); if(speaker) { const QAudioDeviceInfo &defaultDeviceInfo = QAudioDeviceInfo::defaultOutputDevice(); diff --git a/OSBindings/Qt/scantargetwidget.cpp b/OSBindings/Qt/scantargetwidget.cpp index e3fb202ab..97e86fb3a 100644 --- a/OSBindings/Qt/scantargetwidget.cpp +++ b/OSBindings/Qt/scantargetwidget.cpp @@ -13,15 +13,17 @@ void ScanTargetWidget::initializeGL() { glClearColor(0.5, 0.5, 1.0, 1.0); // Follow each swapped frame with an additional update. - connect(this, SIGNAL(frameSwapped()), this, SLOT(update())); + connect(this, &QOpenGLWidget::frameSwapped, this, &ScanTargetWidget::vsync); // qDebug() << "share context: " << bool(context()->shareGroup()); } void ScanTargetWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT); if(scanTarget) { + vsync_predictor_.begin_redraw(); scanTarget->update(width(), height()); scanTarget->draw(width(), height()); + vsync_predictor_.end_redraw(); // static int64_t start = 0; // static int frames = 0; @@ -34,15 +36,26 @@ void ScanTargetWidget::paintGL() { } } +void ScanTargetWidget::vsync() { + vsync_predictor_.announce_vsync(); + + const auto time_now = Time::nanos_now(); + const auto delay_time = ((vsync_predictor_.suggested_draw_time() - time_now) / 1'000'000) - 5; // TODO: the extra 5 is a random guess. + if(delay_time > 0) { + QTimer::singleShot(delay_time, this, SLOT(repaint())); + } else { + repaint(); + } +} + void ScanTargetWidget::resizeGL(int w, int h) { - glViewport(0,0,w,h); + glViewport(0,0,w,h); } Outputs::Display::OpenGL::ScanTarget *ScanTargetWidget::getScanTarget() { makeCurrent(); if(!scanTarget) { scanTarget = std::make_unique(defaultFramebufferObject()); - QTimer::singleShot(500, this, SLOT(update())); // TODO: obviously this is nonsense. } return scanTarget.get(); } diff --git a/OSBindings/Qt/scantargetwidget.h b/OSBindings/Qt/scantargetwidget.h index dbf2f049e..e07218422 100644 --- a/OSBindings/Qt/scantargetwidget.h +++ b/OSBindings/Qt/scantargetwidget.h @@ -4,6 +4,7 @@ #include #include "../../Outputs/OpenGL/ScanTarget.hpp" +#include "../../ClockReceiver/VSyncPredictor.hpp" class ScanTargetWidget : public QOpenGLWidget { @@ -20,8 +21,12 @@ class ScanTargetWidget : public QOpenGLWidget private: // This should be created only once there's an OpenGL context. So it - // can't be done at creation time.4 + // can't be done at creation time. std::unique_ptr scanTarget; + Time::VSyncPredictor vsync_predictor_; + + private slots: + void vsync(); }; #endif // SCANTARGETWIDGET_H