From 764b39a23cf7d013d21f7a3da47f45b6b640f397 Mon Sep 17 00:00:00 2001 From: baldurk Date: Tue, 19 Dec 2017 17:31:32 +0000 Subject: [PATCH] Add update checking system to Qt UI * This only runs on windows - on other platforms we rely on system distribution or user local builds. --- qrenderdoc/Code/QRDUtils.cpp | 71 +++ qrenderdoc/Code/QRDUtils.h | 5 + qrenderdoc/Code/qrenderdoc.cpp | 30 ++ qrenderdoc/Windows/Dialogs/CrashDialog.cpp | 53 +-- qrenderdoc/Windows/Dialogs/SettingsDialog.cpp | 6 + qrenderdoc/Windows/Dialogs/UpdateDialog.cpp | 263 +++++++++++ qrenderdoc/Windows/Dialogs/UpdateDialog.h | 67 +++ qrenderdoc/Windows/Dialogs/UpdateDialog.ui | 423 ++++++++++++++++++ qrenderdoc/Windows/MainWindow.cpp | 252 ++++++++++- qrenderdoc/Windows/MainWindow.h | 20 + qrenderdoc/Windows/MainWindow.ui | 15 +- qrenderdoc/qrenderdoc.pro | 3 + qrenderdoc/qrenderdoc_local.vcxproj | 15 + qrenderdoc/qrenderdoc_local.vcxproj.filters | 15 + renderdoc/api/replay/renderdoc_replay.h | 3 + renderdoc/replay/entry_points.cpp | 92 ++++ 16 files changed, 1283 insertions(+), 50 deletions(-) create mode 100644 qrenderdoc/Windows/Dialogs/UpdateDialog.cpp create mode 100644 qrenderdoc/Windows/Dialogs/UpdateDialog.h create mode 100644 qrenderdoc/Windows/Dialogs/UpdateDialog.ui diff --git a/qrenderdoc/Code/QRDUtils.cpp b/qrenderdoc/Code/QRDUtils.cpp index b69ac1d01..9c57389db 100644 --- a/qrenderdoc/Code/QRDUtils.cpp +++ b/qrenderdoc/Code/QRDUtils.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -1558,6 +1560,75 @@ void ShowProgressDialog(QWidget *window, const QString &labelText, ProgressFinis progressTickerThread.wait(); } +void UpdateTransferProgress(qint64 xfer, qint64 total, QElapsedTimer *timer, + QProgressBar *progressBar, QLabel *progressLabel, QString progressText) +{ + if(xfer >= total) + { + progressBar->setMaximum(10000); + progressBar->setValue(10000); + return; + } + + if(total <= 0) + { + progressBar->setMaximum(10000); + progressBar->setValue(0); + return; + } + + progressBar->setMaximum(10000); + progressBar->setValue(int(10000.0 * (double(xfer) / double(total)))); + + double xferMB = double(xfer) / 1000000.0; + double totalMB = double(total) / 1000000.0; + + double secondsElapsed = double(timer->nsecsElapsed()) * 1.0e-9; + + double speedMBS = xferMB / secondsElapsed; + + qulonglong secondsRemaining = qulonglong(double(totalMB - xferMB) / speedMBS); + + if(secondsElapsed > 1.0) + { + QString remainString; + + qulonglong minutesRemaining = (secondsRemaining / 60) % 60; + qulonglong hoursRemaining = (secondsRemaining / 3600); + secondsRemaining %= 60; + + if(hoursRemaining > 0) + remainString = QFormatStr("%1:%2:%3") + .arg(hoursRemaining, 2, 10, QLatin1Char('0')) + .arg(minutesRemaining, 2, 10, QLatin1Char('0')) + .arg(secondsRemaining, 2, 10, QLatin1Char('0')); + else if(minutesRemaining > 0) + remainString = QFormatStr("%1:%2") + .arg(minutesRemaining, 2, 10, QLatin1Char('0')) + .arg(secondsRemaining, 2, 10, QLatin1Char('0')); + else + remainString = QApplication::translate("qrenderdoc", "%1 seconds").arg(secondsRemaining); + + double speed = speedMBS; + + bool MBs = true; + if(speedMBS < 1) + { + MBs = false; + speed *= 1000; + } + + progressLabel->setText( + QApplication::translate("qrenderdoc", "%1\n%2 MB / %3 MB. %4 remaining (%5 %6)") + .arg(progressText) + .arg(xferMB, 0, 'f', 2) + .arg(totalMB, 0, 'f', 2) + .arg(remainString) + .arg(speed, 0, 'f', 2) + .arg(MBs ? lit("MB/s") : lit("KB/s"))); + } +} + void setEnabledMultiple(const QList &widgets, bool enabled) { for(QWidget *w : widgets) diff --git a/qrenderdoc/Code/QRDUtils.h b/qrenderdoc/Code/QRDUtils.h index 127217a9f..37f789a6b 100644 --- a/qrenderdoc/Code/QRDUtils.h +++ b/qrenderdoc/Code/QRDUtils.h @@ -515,6 +515,8 @@ class QGridLayout; void addGridLines(QGridLayout *grid, QColor gridColor); class QProgressDialog; +class QProgressBar; +class QElapsedTimer; typedef std::function ProgressUpdateMethod; typedef std::function ProgressFinishedMethod; @@ -527,6 +529,9 @@ bool RunProcessAsAdmin(const QString &fullExecutablePath, const QStringList &par void ShowProgressDialog(QWidget *window, const QString &labelText, ProgressFinishedMethod finished, ProgressUpdateMethod update = ProgressUpdateMethod()); +void UpdateTransferProgress(qint64 xfer, qint64 total, QElapsedTimer *timer, + QProgressBar *progressBar, QLabel *progressLabel, QString progressText); + void setEnabledMultiple(const QList &widgets, bool enabled); QString GetSystemUsername(); diff --git a/qrenderdoc/Code/qrenderdoc.cpp b/qrenderdoc/Code/qrenderdoc.cpp index 6578afffd..2019c904a 100644 --- a/qrenderdoc/Code/qrenderdoc.cpp +++ b/qrenderdoc/Code/qrenderdoc.cpp @@ -121,6 +121,29 @@ int main(int argc, char *argv[]) } } + bool updateApplied = false; + + for(int i = 0; i < argc; i++) + { + if(!QString::compare(QString::fromUtf8(argv[i]), lit("--updatefailed"), Qt::CaseInsensitive)) + { + if(i < argc - 1) + RDDialog::critical(NULL, QApplication::translate("qrenderdoc", "Error updating"), + QApplication::translate("qrenderdoc", "Error applying update: %1") + .arg(QString::fromUtf8(argv[i + 1]))); + else + RDDialog::critical(NULL, QApplication::translate("qrenderdoc", "Error updating"), + QApplication::translate("qrenderdoc", "Unknown error applying update")); + } + + if(!QString::compare(QString::fromUtf8(argv[i]), lit("--updatedone"), Qt::CaseInsensitive)) + { + updateApplied = true; + + RENDERDOC_UpdateInstalledVersionNumber(); + } + } + QString remoteHost; uint remoteIdent = 0; @@ -327,6 +350,13 @@ int main(int argc, char *argv[]) } } + if(updateApplied) + { + config.CheckUpdate_UpdateAvailable = false; + config.CheckUpdate_UpdateResponse = ""; + config.Save(); + } + while(ctx.isRunning()) { application.processEvents(QEventLoop::WaitForMoreEvents); diff --git a/qrenderdoc/Windows/Dialogs/CrashDialog.cpp b/qrenderdoc/Windows/Dialogs/CrashDialog.cpp index a9ab90bd8..c828aeb68 100644 --- a/qrenderdoc/Windows/Dialogs/CrashDialog.cpp +++ b/qrenderdoc/Windows/Dialogs/CrashDialog.cpp @@ -144,7 +144,6 @@ void CrashDialog::resizeEvent(QResizeEvent *) { recentre(); } - void CrashDialog::recentre() { QRect scr = QApplication::desktop()->screenGeometry(); @@ -187,10 +186,11 @@ void CrashDialog::on_send_clicked() if(ui->captureUpload->isChecked()) { QMessageBox::StandardButton result = RDDialog::question( - this, tr("Are you sure?"), - tr("Uploading your capture file will send it privately to the RenderDoc server where I can " - "use it to reproduce your problem.\n\nAre you sure you are OK with sending the capture " - "securely to RenderDoc's website?")); + this, tr("Are you sure?"), tr("Uploading your capture file will send it privately to the " + "RenderDoc server where I can " + "use it to reproduce your problem.\n\nAre you sure you are " + "OK with sending the capture " + "securely to RenderDoc's website?")); if(result != QMessageBox::Yes) { @@ -334,7 +334,6 @@ void CrashDialog::sendReport() ui->uploadRetry->setEnabled(true); }); - ui->progressBar->setMaximum(10000); ui->progressBar->setValue(0); ui->progressText->setText(tr("Uploading report...\nCalculating time remaining")); @@ -344,46 +343,8 @@ void CrashDialog::sendReport() m_UploadTimer->start(); QObject::connect(m_Request, &QNetworkReply::uploadProgress, [this](qint64 sent, qint64 total) { - if(total > 0 && total > sent) - { - ui->progressBar->setValue(int(10000.0 * (double(sent) / double(total)))); - - double sentMB = double(sent) / 1000000.0; - double totalMB = double(total) / 1000000.0; - - double secondsElapsed = double(m_UploadTimer->nsecsElapsed()) * 1.0e-9; - - double speedMBS = sentMB / secondsElapsed; - - qulonglong secondsRemaining = qulonglong(double(totalMB - sentMB) / speedMBS); - - if(secondsElapsed > 1.0) - { - QString remainString; - - qulonglong minutesRemaining = (secondsRemaining / 60) % 60; - qulonglong hoursRemaining = (secondsRemaining / 3600); - secondsRemaining %= 60; - - if(hoursRemaining > 0) - remainString = QFormatStr("%1:%2:%3") - .arg(hoursRemaining, 2, 10, QLatin1Char('0')) - .arg(minutesRemaining, 2, 10, QLatin1Char('0')) - .arg(secondsRemaining, 2, 10, QLatin1Char('0')); - else if(minutesRemaining > 0) - remainString = QFormatStr("%1:%2") - .arg(minutesRemaining, 2, 10, QLatin1Char('0')) - .arg(secondsRemaining, 2, 10, QLatin1Char('0')); - else - remainString = tr("%1 seconds").arg(secondsRemaining); - - ui->progressText->setText(tr("Uploading report...\n%1 MB / %2 MB. %3 remaining (%4 MB/s)") - .arg(sentMB, 0, 'f', 2) - .arg(totalMB, 0, 'f', 2) - .arg(remainString) - .arg(speedMBS, 0, 'f', 2)); - } - } + UpdateTransferProgress(sent, total, m_UploadTimer, ui->progressBar, ui->progressText, + tr("Uploading report...")); }); QObject::connect(m_Request, &QNetworkReply::finished, [this]() { diff --git a/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp b/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp index 982089c48..92e07ca43 100644 --- a/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp +++ b/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp @@ -222,6 +222,12 @@ void SettingsDialog::on_CheckUpdate_AllowChecks_toggled(bool checked) { m_Ctx.Config().CheckUpdate_AllowChecks = ui->CheckUpdate_AllowChecks->isChecked(); + if(!m_Ctx.Config().CheckUpdate_AllowChecks) + { + m_Ctx.Config().CheckUpdate_UpdateAvailable = false; + m_Ctx.Config().CheckUpdate_UpdateResponse = ""; + } + m_Ctx.Config().Save(); } diff --git a/qrenderdoc/Windows/Dialogs/UpdateDialog.cpp b/qrenderdoc/Windows/Dialogs/UpdateDialog.cpp new file mode 100644 index 000000000..0274b7928 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/UpdateDialog.cpp @@ -0,0 +1,263 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2017 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "UpdateDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Code/QRDUtils.h" +#include "ui_UpdateDialog.h" +#include "version.h" + +UpdateDialog::UpdateDialog(QString updateResponse, QWidget *parent) + : QDialog(parent), ui(new Ui::UpdateDialog) +{ + ui->setupUi(this); + + ui->updateText->setBackgroundRole(QPalette::Base); + ui->updateText->setForegroundRole(QPalette::Text); + + m_NetManager = new QNetworkAccessManager(this); + + setWindowFlags((windowFlags() | Qt::MSWindowsFixedSizeDialogHint) & + ~Qt::WindowContextHelpButtonHint); + + QStringList lines = updateResponse.split(QLatin1Char('\n'), QString::SkipEmptyParts); + + m_NewVer = lines[0]; + m_URL = lines[1]; + m_Size = lines[2].toUInt(); + + QString notes; + + for(int i = 3; i < lines.count(); i++) + notes += lines[i]; + + ui->progressText->setVisible(false); + ui->progressBar->setVisible(false); + + QString text = tr("Update Available - v%1").arg(m_NewVer); + ui->updateVer->setText(text); + setWindowTitle(text); + + ui->updateText->setText(notes); + + ui->currentVersion->setText(lit(FULL_VERSION_STRING)); + ui->newVersion->setText(QFormatStr("v%1").arg(m_NewVer)); + ui->downloadSize->setText(QFormatStr("%1 MB").arg(double(m_Size) / 1000000.0, 0, 'f', 2)); + + adjustSize(); +} + +UpdateDialog::~UpdateDialog() +{ + delete m_DownloadTimer; + + delete ui; +} + +void UpdateDialog::keyPressEvent(QKeyEvent *e) +{ + if(e->key() == Qt::Key_Escape) + return; + + QDialog::keyPressEvent(e); +} + +void UpdateDialog::closeEvent(QCloseEvent *e) +{ + if(ui->close->isEnabled()) + { + QDialog::closeEvent(e); + return; + } + + e->ignore(); + return; +} + +void UpdateDialog::on_releaseNotes_clicked() +{ + QDesktopServices::openUrl( + QUrl(lit("https://github.com/baldurk/renderdoc/releases/tag/v%1").arg(m_NewVer))); +} + +void UpdateDialog::on_close_clicked() +{ + reject(); +} + +void UpdateDialog::on_update_clicked() +{ + QMessageBox::StandardButton res = RDDialog::question( + this, tr("RenderDoc Update"), tr("This will close RenderDoc immediately - if you have any " + "unsaved work, save it first!\n" + "Continue?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + + if(res == QMessageBox::Yes) + { + QString runningPrograms; + int running = 0; + + uint32_t nextIdent = 0; + + QString localhost = lit("localhost"); + + for(;;) + { + // just a sanity check to make sure we don't hit some unexpected case and infinite loop + uint32_t prevIdent = nextIdent; + + nextIdent = RENDERDOC_EnumerateRemoteTargets("localhost", nextIdent); + + if(nextIdent == 0 || prevIdent >= nextIdent) + break; + + running++; + + ITargetControl *conn = RENDERDOC_CreateTargetControl("localhost", nextIdent, "updater", false); + + if(conn) + { + if(!runningPrograms.isEmpty()) + runningPrograms += lit("\n"); + + QString target = + conn->GetTarget() ? QString::fromUtf8(conn->GetTarget()) : lit(""); + if(conn->GetAPI()) + runningPrograms += tr("%1 running %2").arg(target).arg(QString::fromUtf8(conn->GetAPI())); + else + runningPrograms += target; + + conn->Shutdown(); + } + } + + if(running > 0) + { + RDDialog::critical( + this, tr("RenderDoc in use"), + tr("RenderDoc is currently capturing, cannot update until the program%1 closed:\n\n") + .arg(running > 1 ? lit("s are") : lit(" is")) + + runningPrograms); + return; + } + + ui->metadataFrame->setVisible(false); + ui->progressBar->setVisible(true); + ui->progressText->setVisible(true); + + ui->progressBar->setMaximum(10000); + ui->progressBar->setValue(0); + ui->progressText->setText(tr("Preparing Download")); + + ui->close->setEnabled(false); + ui->update->setEnabled(false); + + delete m_DownloadTimer; + m_DownloadTimer = new QElapsedTimer(); + + m_DownloadTimer->start(); + + QNetworkReply *req = m_NetManager->get(QNetworkRequest(QUrl(m_URL))); + + QObject::connect(req, &QNetworkReply::downloadProgress, [this](qint64 recvd, qint64 total) { + UpdateTransferProgress(recvd, total, m_DownloadTimer, ui->progressBar, ui->progressText, + tr("Downloading update...")); + }); + + QObject::connect(req, OverloadedSlot::of(&QNetworkReply::error), + [this, req](QNetworkReply::NetworkError err) { + ui->progressBar->setValue(0); + ui->progressText->setText(tr("Network error:\n%1").arg(req->errorString())); + ui->update->setEnabled(true); + ui->close->setEnabled(true); + ui->update->setText(tr("Retry Update")); + }); + + QObject::connect(req, &QNetworkReply::finished, [this, req]() { + + // don't do anything if we're finished after an error + if(ui->update->isEnabled()) + return; + + QDir dir(QDir::tempPath()); + + dir.mkdir(lit("RenderDocUpdate")); + dir.cd(lit("RenderDocUpdate")); + + QString path = dir.absoluteFilePath(lit("update.zip")); + + { + QFile file(path); + if(file.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + file.write(req->readAll()); + } + else + { + RDDialog::critical(this, tr("Error saving file"), + tr("Couldn't save update file to: %1").arg(path)); + reject(); + } + } + + QDir appDir = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir(); + + bool success = true; + + QString dll = lit("renderdoc.dll"); + QString cmd = lit("renderdoccmd.exe"); + + QFile::remove(dir.absoluteFilePath(dll)); + QFile::remove(dir.absoluteFilePath(cmd)); + + success &= QFile::copy(appDir.absoluteFilePath(dll), dir.absoluteFilePath(dll)); + success &= QFile::copy(appDir.absoluteFilePath(cmd), dir.absoluteFilePath(cmd)); + + if(!success) + { + RDDialog::critical(this, tr("Error running updated"), + tr("Couldn't copy updater files to temporary path")); + reject(); + } + + QDir::setCurrent(dir.absolutePath()); + + success = RunProcessAsAdmin(dir.absoluteFilePath(cmd), QStringList() + << lit("upgrade") << lit("--path") + << appDir.absolutePath()); + + exit(0); + + }); + } +} \ No newline at end of file diff --git a/qrenderdoc/Windows/Dialogs/UpdateDialog.h b/qrenderdoc/Windows/Dialogs/UpdateDialog.h new file mode 100644 index 000000000..36ce0b4a4 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/UpdateDialog.h @@ -0,0 +1,67 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2017 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include + +namespace Ui +{ +class UpdateDialog; +} + +class QNetworkAccessManager; +class QNetworkReply; +class QElapsedTimer; + +struct Thumbnail; + +class UpdateDialog : public QDialog +{ + Q_OBJECT +public: + explicit UpdateDialog(QString updateResponse, QWidget *parent = 0); + ~UpdateDialog(); + +private slots: + // automatic slots + void on_releaseNotes_clicked(); + void on_close_clicked(); + void on_update_clicked(); + +private: + void keyPressEvent(QKeyEvent *e) override; + void closeEvent(QCloseEvent *) override; + + QString m_NewVer; + QString m_URL; + uint32_t m_Size = 0; + + Ui::UpdateDialog *ui; + + QElapsedTimer *m_DownloadTimer = NULL; + + QNetworkAccessManager *m_NetManager; + QNetworkReply *m_Request = NULL; +}; diff --git a/qrenderdoc/Windows/Dialogs/UpdateDialog.ui b/qrenderdoc/Windows/Dialogs/UpdateDialog.ui new file mode 100644 index 000000000..0285abcb7 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/UpdateDialog.ui @@ -0,0 +1,423 @@ + + + UpdateDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 465 + 505 + + + + + 0 + 0 + + + + Update Available + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 59 + 183 + 121 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 59 + 183 + 121 + + + + + + + + + 106 + 104 + 100 + + + + + + + 59 + 183 + 121 + + + + + + + 59 + 183 + 121 + + + + + + + + true + + + + 20 + + + 20 + + + + + + 0 + 0 + + + + + 128 + 128 + + + + + 128 + 128 + + + + + + + :/logo.svg + + + true + + + + + + + + 0 + 0 + + + + + + + + + + + + 20 + + + + Update Available - vX.YZ + + + + + + + + + + 4 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 0 + 200 + + + + + 16777215 + 200 + + + + true + + + QFrame::Box + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + 6 + + + true + + + Qt::TextBrowserInteraction + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 10 + + + + + New Version: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Download Size: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + vX.YY + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + vX.YZ + + + + + + + 12.34 MB + + + + + + + Current Version: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Downloading + + + true + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Full Release Notes + + + + + + + Install Update + + + + + + + Close + + + + + + + + + + + + RDLabel + QLabel +
Widgets/Extended/RDLabel.h
+
+
+ + + + +
diff --git a/qrenderdoc/Windows/MainWindow.cpp b/qrenderdoc/Windows/MainWindow.cpp index a42ed0a5c..92e861da3 100644 --- a/qrenderdoc/Windows/MainWindow.cpp +++ b/qrenderdoc/Windows/MainWindow.cpp @@ -47,6 +47,7 @@ #include "Windows/Dialogs/SettingsDialog.h" #include "Windows/Dialogs/SuggestRemoteDialog.h" #include "Windows/Dialogs/TipsDialog.h" +#include "Windows/Dialogs/UpdateDialog.h" #include "ui_MainWindow.h" #include "version.h" @@ -180,11 +181,30 @@ MainWindow::MainWindow(ICaptureContext &ctx) : QMainWindow(NULL), ui(new Ui::Mai ui->action_Send_Error_Report->setEnabled(false); #endif + m_NetManager = new QNetworkAccessManager(this); + +#if !defined(Q_OS_WIN32) + // update checks only happen on windows + { + QList actions = ui->menu_Help->actions(); + int idx = actions.indexOf(ui->action_Update_Available); + idx++; + if(idx < actions.count() && actions[idx]->isSeparator()) + delete actions[idx]; + + delete ui->action_Update_Available; + ui->action_Update_Available = NULL; + + delete ui->action_Check_for_Updates; + ui->action_Check_for_Updates = NULL; + } +#endif + PopulateRecentCaptureFiles(); PopulateRecentCaptureSettings(); PopulateReportedBugs(); - m_NetManager = new QNetworkAccessManager(this); + CheckUpdates(); rdcarray bugs = m_Ctx.Config().CrashReport_ReportedBugs; LambdaThread *bugupdate = new LambdaThread([this, bugs]() { @@ -854,6 +874,47 @@ void MainWindow::SetTitle() SetTitle(m_Ctx.GetCaptureFilename()); } +bool MainWindow::HandleMismatchedVersions() +{ + if(IsVersionMismatched()) + { + qCritical() << "Version mismatch between UI (" << lit(MAJOR_MINOR_VERSION_STRING) << ")" + << "and core" + << "(" << QString::fromUtf8(RENDERDOC_GetVersionString()) << ")"; + +#if !RENDERDOC_OFFICIAL_BUILD + RDDialog::critical( + this, tr("Unofficial build - mismatched versions"), + tr("You are running an unofficial build with mismatched core and UI versions.\n" + "Double check where you got your build from and do a sanity check!")); +#else + QMessageBox::StandardButton res = RDDialog::critical( + this, tr("Mismatched versions"), + tr("RenderDoc has detected mismatched versions between its internal module and UI.\n" + "This is likely caused by a buggy update in the past which partially updated your " + "install." + "Likely because a program was running with renderdoc while the update happened.\n" + "You should reinstall RenderDoc immediately as this configuration is almost guaranteed " + "to crash.\n\n" + "Would you like to open the downloads page to reinstall?"), + QMessageBox::Yes | QMessageBox::No); + + if(res == QMessageBox::Yes) + QDesktopServices::openUrl(QUrl(lit("https://renderdoc.org/builds"))); + + SetUpdateAvailable(); +#endif + return true; + } + + return false; +} + +bool MainWindow::IsVersionMismatched() +{ + return QString::fromLatin1(RENDERDOC_GetVersionString()) != lit(MAJOR_MINOR_VERSION_STRING); +} + void MainWindow::ClearRecentCaptureFiles() { m_Ctx.Config().RecentCaptureFiles.clear(); @@ -952,16 +1013,152 @@ void MainWindow::PopulateReportedBugs() if(unread) { - ui->menu_Help->setIcon(Icons::bug()); + if(!m_Ctx.Config().CheckUpdate_UpdateAvailable) + ui->menu_Help->setIcon(Icons::bug()); ui->menu_Reported_Bugs->setIcon(Icons::bug()); } else { - ui->menu_Help->setIcon(QIcon()); + if(!m_Ctx.Config().CheckUpdate_UpdateAvailable) + ui->menu_Help->setIcon(QIcon()); ui->menu_Reported_Bugs->setIcon(QIcon()); } } +void MainWindow::CheckUpdates(bool forceCheck, UpdateResultMethod callback) +{ + if(!ui->action_Update_Available) + return; + + bool mismatch = HandleMismatchedVersions(); + if(mismatch) + return; + + if(!forceCheck && !m_Ctx.Config().CheckUpdate_AllowChecks) + { + ui->action_Update_Available->setText(tr("Update checks disabled")); + ui->action_Update_Available->setEnabled(false); + if(callback) + callback(UpdateResult::Disabled); + return; + } + +#if RENDERDOC_OFFICIAL_BUILD + if(m_Ctx.Config().CheckUpdate_UpdateAvailable) + { + if(m_Ctx.Config().CheckUpdate_UpdateResponse.isEmpty()) + { + forceCheck = true; + } + else if(!forceCheck) + { + SetUpdateAvailable(); + return; + } + } + + QDateTime today = QDateTime::currentDateTime(); + QDateTime compare = today.addDays(-2); + + qint64 diff = compare.secsTo(m_Ctx.Config().CheckUpdate_LastUpdate); + + if(!forceCheck && diff > 0) + { + if(callback) + callback(UpdateResult::Toosoon); + return; + } + + m_Ctx.Config().CheckUpdate_LastUpdate = today; + m_Ctx.Config().Save(); + +#if QT_POINTER_SIZE == 4 + QString bitness = lit("32"); +#else + QString bitness = lit("64"); +#endif + QString versionCheck = lit(MAJOR_MINOR_VERSION_STRING); + + statusText->setText(tr("Checking for updates...")); + + statusProgress->setVisible(true); + statusProgress->setMinimumSize(QSize(200, 0)); + statusProgress->setMinimum(0); + statusProgress->setMaximum(0); + + // call out to the status-check to see when the bug report was last updated + QNetworkReply *req = m_NetManager->get(QNetworkRequest(QUrl( + lit("https://renderdoc.org/getupdateurl/%1/%2?htmlnotes=1").arg(bitness).arg(versionCheck)))); + + QObject::connect(req, OverloadedSlot::of(&QNetworkReply::error), + [this, req](QNetworkReply::NetworkError) { + qCritical() << "Network error:" << req->errorString(); + }); + + QObject::connect(req, &QNetworkReply::finished, [this, req, callback]() { + QString response = QString::fromUtf8(req->readAll()); + + statusText->setText(QString()); + statusProgress->setVisible(false); + statusProgress->setMaximum(1000); + + if(response.isEmpty()) + { + m_Ctx.Config().CheckUpdate_UpdateAvailable = false; + m_Ctx.Config().CheckUpdate_UpdateResponse = ""; + m_Ctx.Config().Save(); + SetNoUpdate(); + + if(callback) + callback(UpdateResult::Latest); + + return; + } + + m_Ctx.Config().CheckUpdate_UpdateAvailable = true; + m_Ctx.Config().CheckUpdate_UpdateResponse = response; + m_Ctx.Config().Save(); + SetUpdateAvailable(); + UpdatePopup(); + }); +#else //! RENDERDOC_OFFICIAL_BUILD + { + if(callback) + callback(UpdateResult::Unofficial); + return; + } +#endif +} + +void MainWindow::SetUpdateAvailable() +{ + if(!ui->action_Update_Available) + return; + + ui->menu_Help->setIcon(Icons::hourglass()); + ui->action_Update_Available->setEnabled(true); + ui->action_Update_Available->setText(tr("An update is available")); +} + +void MainWindow::SetNoUpdate() +{ + if(!ui->action_Update_Available) + return; + + ui->menu_Help->setIcon(QIcon()); + ui->action_Update_Available->setEnabled(false); + ui->action_Update_Available->setText(tr("No update available")); +} + +void MainWindow::UpdatePopup() +{ + if(!m_Ctx.Config().CheckUpdate_UpdateAvailable || !m_Ctx.Config().CheckUpdate_AllowChecks) + return; + + UpdateDialog update((QString)m_Ctx.Config().CheckUpdate_UpdateResponse); + RDDialog::show(&update); +} + void MainWindow::ShowLiveCapture(LiveCapture *live) { m_LiveCaptures.push_back(live); @@ -1124,6 +1321,7 @@ void MainWindow::setProgress(float val) else { statusProgress->setVisible(true); + statusProgress->setMaximum(1000); statusProgress->setValue(1000 * val); } } @@ -1972,6 +2170,54 @@ void MainWindow::on_action_Send_Error_Report_triggered() QFile::remove(QString(report)); } +void MainWindow::on_action_Check_for_Updates_triggered() +{ + CheckUpdates(true, [this](UpdateResult updateResult) { + switch(updateResult) + { + case UpdateResult::Disabled: + case UpdateResult::Toosoon: + { + // won't happen, we forced the check + break; + } + case UpdateResult::Unofficial: + { + QMessageBox::StandardButton res = + RDDialog::question(this, tr("Unofficial build"), + tr("You are running an unofficial build, not a stable release.\n" + "Updates are only available for installed release builds\n\n" + "Would you like to open the builds list in a browser?")); + + if(res == QMessageBox::Yes) + QDesktopServices::openUrl(lit("https://renderdoc.org/builds")); + break; + } + case UpdateResult::Latest: + { + RDDialog::information(this, tr("Latest version"), + tr("You are running the latest version.")); + break; + } + case UpdateResult::Upgrade: + { + // CheckUpdates() will have shown a dialog for this + break; + } + } + }); +} + +void MainWindow::on_action_Update_Available_triggered() +{ + bool mismatch = HandleMismatchedVersions(); + if(mismatch) + return; + + SetUpdateAvailable(); + UpdatePopup(); +} + void MainWindow::saveLayout_triggered() { LoadSaveLayout(qobject_cast(QObject::sender()), true); diff --git a/qrenderdoc/Windows/MainWindow.h b/qrenderdoc/Windows/MainWindow.h index adea64158..9f7289d09 100644 --- a/qrenderdoc/Windows/MainWindow.h +++ b/qrenderdoc/Windows/MainWindow.h @@ -138,6 +138,8 @@ private slots: void on_action_Counter_Viewer_triggered(); void on_action_Resource_Inspector_triggered(); void on_action_Send_Error_Report_triggered(); + void on_action_Check_for_Updates_triggered(); + void on_action_Update_Available_triggered(); // manual slots void saveLayout_triggered(); @@ -162,6 +164,17 @@ private: QString dragFilename(const QMimeData *mimeData); + enum class UpdateResult + { + Disabled, + Unofficial, + Toosoon, + Latest, + Upgrade, + }; + + typedef std::function UpdateResultMethod; + Ui::MainWindow *ui; ICaptureContext &m_Ctx; @@ -188,6 +201,13 @@ private: QString m_LastSaveCapturePath; + void CheckUpdates(bool forceCheck = false, UpdateResultMethod callback = UpdateResultMethod()); + void SetUpdateAvailable(); + void SetNoUpdate(); + void UpdatePopup(); + bool HandleMismatchedVersions(); + bool IsVersionMismatched(); + void setCaptureHasErrors(bool errors); void SetTitle(const QString &filename); diff --git a/qrenderdoc/Windows/MainWindow.ui b/qrenderdoc/Windows/MainWindow.ui index b7a327b2a..ada8d5a89 100644 --- a/qrenderdoc/Windows/MainWindow.ui +++ b/qrenderdoc/Windows/MainWindow.ui @@ -154,6 +154,7 @@ + @@ -364,8 +365,15 @@ + + false + + + + :/hourglass.png:/hourglass.png + - Update Available + No update available @@ -443,6 +451,11 @@ &Clear Reported Bugs + + + Check for updates + + diff --git a/qrenderdoc/qrenderdoc.pro b/qrenderdoc/qrenderdoc.pro index e8e9bc4c3..8353852e8 100644 --- a/qrenderdoc/qrenderdoc.pro +++ b/qrenderdoc/qrenderdoc.pro @@ -172,6 +172,7 @@ SOURCES += Code/qrenderdoc.cpp \ Styles/RDTweakedNativeStyle/RDTweakedNativeStyle.cpp \ Windows/Dialogs/AboutDialog.cpp \ Windows/Dialogs/CrashDialog.cpp \ + Windows/Dialogs/UpdateDialog.cpp \ Windows/MainWindow.cpp \ Windows/EventBrowser.cpp \ Windows/TextureViewer.cpp \ @@ -244,6 +245,7 @@ HEADERS += Code/CaptureContext.h \ Styles/RDTweakedNativeStyle/RDTweakedNativeStyle.h \ Windows/Dialogs/AboutDialog.h \ Windows/Dialogs/CrashDialog.h \ + Windows/Dialogs/UpdateDialog.h \ Windows/MainWindow.h \ Windows/EventBrowser.h \ Windows/TextureViewer.h \ @@ -299,6 +301,7 @@ HEADERS += Code/CaptureContext.h \ Windows/Dialogs/AnalyticsPromptDialog.h FORMS += Windows/Dialogs/AboutDialog.ui \ Windows/Dialogs/CrashDialog.ui \ + Windows/Dialogs/UpdateDialog.ui \ Windows/MainWindow.ui \ Windows/EventBrowser.ui \ Windows/TextureViewer.ui \ diff --git a/qrenderdoc/qrenderdoc_local.vcxproj b/qrenderdoc/qrenderdoc_local.vcxproj index d16571e14..dfb8c999a 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj +++ b/qrenderdoc/qrenderdoc_local.vcxproj @@ -578,6 +578,7 @@ + @@ -690,6 +691,7 @@ + @@ -888,6 +890,7 @@ + @@ -1128,6 +1131,12 @@ MOC %(Filename).h $(IntDir)generated\moc_%(Filename).cpp + + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe;%(AdditionalInputs) + "$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe" -DUNICODE -DWIN32 -DWIN64 -D_WIN32 -D_WIN64 -DRENDERDOC_PLATFORM_WIN32 -DSCINTILLA_QT=1 -DSCI_LEXER=1 -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -D_MSC_VER=1900 -I"$(ProjectDir)." -I"$(SolutionDir)\renderdoc\api\replay" -I"$(ProjectDir)3rdparty\qt\$(Platform)\mkspecs/win32-msvc2015" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtWidgets" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtGui" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtCore" "%(Fullpath)" -o "$(IntDir)generated\moc_%(Filename).cpp" + MOC %(Filename).h + $(IntDir)generated\moc_%(Filename).cpp + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe;%(AdditionalInputs) "$(ProjectDir)3rdparty\qt\$(Platform)\bin\moc.exe" -DUNICODE -DWIN32 -DWIN64 -D_WIN32 -D_WIN64 -DRENDERDOC_PLATFORM_WIN32 -DSCINTILLA_QT=1 -DSCI_LEXER=1 -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -D_MSC_VER=1900 -I"$(ProjectDir)." -I"$(SolutionDir)\renderdoc\api\replay" -I"$(ProjectDir)3rdparty\qt\$(Platform)\mkspecs/win32-msvc2015" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtWidgets" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtGui" -I"$(ProjectDir)3rdparty\qt\$(Platform)\include\QtCore" "%(Fullpath)" -o "$(IntDir)generated\moc_%(Filename).cpp" @@ -1374,6 +1383,12 @@ UIC %(Filename).ui $(IntDir)generated\ui_%(Filename).h + + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe;%(AdditionalInputs) + "$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe" "%(Fullpath)" -o "$(IntDir)generated\ui_%(Filename).h" + UIC %(Filename).ui + $(IntDir)generated\ui_%(Filename).h + %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe;%(AdditionalInputs) "$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe" "%(Fullpath)" -o "$(IntDir)generated\ui_%(Filename).h" diff --git a/qrenderdoc/qrenderdoc_local.vcxproj.filters b/qrenderdoc/qrenderdoc_local.vcxproj.filters index b79cd3541..77f4bb8ba 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj.filters +++ b/qrenderdoc/qrenderdoc_local.vcxproj.filters @@ -696,6 +696,12 @@ Windows\Dialogs + + Generated Files + + + Windows\Dialogs + @@ -1049,6 +1055,9 @@ Generated Files + + Generated Files + @@ -1415,6 +1424,12 @@ Windows\Dialogs + + Windows\Dialogs + + + Windows\Dialogs + diff --git a/renderdoc/api/replay/renderdoc_replay.h b/renderdoc/api/replay/renderdoc_replay.h index 2ebfb4103..97e5efd13 100644 --- a/renderdoc/api/replay/renderdoc_replay.h +++ b/renderdoc/api/replay/renderdoc_replay.h @@ -1876,6 +1876,9 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UpdateVulkanLayerRegistrati // Miscellaneous! ////////////////////////////////////////////////////////////////////////// +DOCUMENT("Internal function for updating installed version number in windows registry."); +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UpdateInstalledVersionNumber(); + DOCUMENT("Internal function for initialising global process environment in a replay program."); extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_InitGlobalEnv(GlobalEnvironment env, const rdcarray &args); diff --git a/renderdoc/replay/entry_points.cpp b/renderdoc/replay/entry_points.cpp index c4de46aea..99fe1d965 100644 --- a/renderdoc/replay/entry_points.cpp +++ b/renderdoc/replay/entry_points.cpp @@ -498,6 +498,98 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UpdateVulkanLayerRegistrati RenderDoc::Inst().UpdateVulkanLayerRegistration(systemLevel); } +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UpdateInstalledVersionNumber() +{ +#if ENABLED(RDOC_WIN32) + HKEY key = NULL; + + LSTATUS ret = + RegCreateKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", + 0, NULL, 0, KEY_READ | KEY_WRITE, NULL, &key, NULL); + + if(ret != ERROR_SUCCESS) + { + if(key) + RegCloseKey(key); + + return; + } + + bool done = false; + + char guidName[256] = {}; + for(DWORD idx = 0; ret == ERROR_SUCCESS && !done; idx++) + { + // enumerate all the uninstall keys + ret = RegEnumKeyA(key, idx, guidName, sizeof(guidName) - 1); + + if(ret == ERROR_NO_MORE_ITEMS) + { + break; + } + else if(ret != ERROR_SUCCESS) + { + break; + } + + // open the key as we'll need it for RegSetValueExA + HKEY subkey = NULL; + ret = RegCreateKeyExA(key, guidName, 0, NULL, 0, KEY_READ | KEY_WRITE, NULL, &subkey, NULL); + + if(ret == ERROR_SUCCESS && subkey) + { + char DisplayName[256] = {}; + char Publisher[256] = {}; + DWORD len = sizeof(DisplayName) - 1; + + // fetch DisplayName and Publisher values + ret = RegGetValueA(subkey, NULL, "DisplayName", RRF_RT_ANY, NULL, DisplayName, &len); + + // allow the value to silently not exist + if(ret != ERROR_SUCCESS) + { + DisplayName[0] = 0; + ret = ERROR_SUCCESS; + } + + len = sizeof(Publisher) - 1; + ret = RegGetValueA(subkey, NULL, "Publisher", RRF_RT_ANY, NULL, Publisher, &len); + + if(ret != ERROR_SUCCESS) + { + Publisher[0] = 0; + ret = ERROR_SUCCESS; + } + + // if this is our key, set the version number + if(!strcmp(DisplayName, "RenderDoc") && !strcmp(Publisher, "Baldur Karlsson")) + { + DWORD Version = (RENDERDOC_VERSION_MAJOR << 24) | (RENDERDOC_VERSION_MINOR << 16); + DWORD VersionMajor = RENDERDOC_VERSION_MAJOR; + DWORD VersionMinor = RENDERDOC_VERSION_MINOR; + std::string DisplayVersion = MAJOR_MINOR_VERSION_STRING ".0"; + + RegSetValueExA(subkey, "Version", 0, REG_DWORD, (const BYTE *)&Version, sizeof(Version)); + RegSetValueExA(subkey, "VersionMajor", 0, REG_DWORD, (const BYTE *)&VersionMajor, + sizeof(VersionMajor)); + RegSetValueExA(subkey, "VersionMinor", 0, REG_DWORD, (const BYTE *)&VersionMinor, + sizeof(VersionMinor)); + RegSetValueExA(subkey, "DisplayVersion", 0, REG_SZ, (const BYTE *)DisplayVersion.c_str(), + (DWORD)DisplayVersion.size() + 1); + done = true; + } + } + + if(subkey) + RegCloseKey(subkey); + } + + if(key) + RegCloseKey(key); + +#endif +} + static std::string ResourceFormatName(const ResourceFormat &fmt) { std::string ret;