diff --git a/qrenderdoc/Code/CaptureContext.cpp b/qrenderdoc/Code/CaptureContext.cpp index f5be034e2..1e0be173c 100644 --- a/qrenderdoc/Code/CaptureContext.cpp +++ b/qrenderdoc/Code/CaptureContext.cpp @@ -60,6 +60,8 @@ CaptureContext::CaptureContext(QString paramFilename, QString remoteHost, uint32 m_CaptureLoaded = false; m_LoadInProgress = false; + RENDERDOC_RegisterMemoryRegion(this, sizeof(CaptureContext)); + memset(&m_APIProps, 0, sizeof(m_APIProps)); m_CurD3D11PipelineState = &m_DummyD3D11; @@ -93,6 +95,7 @@ CaptureContext::CaptureContext(QString paramFilename, QString remoteHost, uint32 CaptureContext::~CaptureContext() { + RENDERDOC_UnregisterMemoryRegion(this); delete m_Icon; m_Renderer.CloseThread(); delete m_MainWindow; @@ -129,6 +132,9 @@ void CaptureContext::LoadCapture(const rdcstr &captureFile, const rdcstr &origFi { m_LoadInProgress = true; + if(local) + m_Config.CrashReport_LastOpenedCapture = origFilename; + bool newCapture = (!temporary && !Config().RecentCaptureFiles.contains(origFilename)); LambdaThread *thread = new LambdaThread([this, captureFile, origFilename, temporary, local]() { @@ -793,6 +799,8 @@ void CaptureContext::CloseCapture() if(!m_CaptureLoaded) return; + m_Config.CrashReport_LastOpenedCapture = QString(); + m_CaptureTemporary = false; m_CaptureFile = QString(); diff --git a/qrenderdoc/Code/Interface/PersistantConfig.cpp b/qrenderdoc/Code/Interface/PersistantConfig.cpp index 75fe8416f..2c8f5621c 100644 --- a/qrenderdoc/Code/Interface/PersistantConfig.cpp +++ b/qrenderdoc/Code/Interface/PersistantConfig.cpp @@ -385,3 +385,28 @@ SPIRVDisassembler::operator QVariant() const return map; } + +BugReport::BugReport(const QVariant &var) +{ + QVariantMap map = var.toMap(); + if(map.contains(lit("ID"))) + ID = map[lit("ID")].toString(); + if(map.contains(lit("SubmitDate"))) + SubmitDate = map[lit("SubmitDate")].toDateTime(); + if(map.contains(lit("CheckDate"))) + CheckDate = map[lit("CheckDate")].toDateTime(); + if(map.contains(lit("UnreadUpdates"))) + UnreadUpdates = map[lit("UnreadUpdates")].toBool(); +} + +BugReport::operator QVariant() const +{ + QVariantMap map; + + map[lit("ID")] = ID; + map[lit("SubmitDate")] = SubmitDate; + map[lit("CheckDate")] = CheckDate; + map[lit("UnreadUpdates")] = UnreadUpdates; + + return map; +} diff --git a/qrenderdoc/Code/Interface/PersistantConfig.h b/qrenderdoc/Code/Interface/PersistantConfig.h index 3504ab120..71437cc6f 100644 --- a/qrenderdoc/Code/Interface/PersistantConfig.h +++ b/qrenderdoc/Code/Interface/PersistantConfig.h @@ -59,6 +59,50 @@ struct SPIRVDisassembler DECLARE_REFLECTION_STRUCT(SPIRVDisassembler); +#define BUGREPORT_URL "https://renderdoc.org/bugreporter" + +DOCUMENT("Describes a submitted bug report."); +struct BugReport +{ + DOCUMENT(""); + BugReport() { UnreadUpdates = false; } + VARIANT_CAST(BugReport); + bool operator==(const BugReport &o) const + { + return ID == o.ID && SubmitDate == o.SubmitDate && CheckDate == o.CheckDate && + UnreadUpdates == o.UnreadUpdates; + } + bool operator<(const BugReport &o) const + { + if(ID != o.ID) + return ID < o.ID; + if(SubmitDate != o.SubmitDate) + return SubmitDate < o.SubmitDate; + if(CheckDate != o.CheckDate) + return CheckDate < o.CheckDate; + if(UnreadUpdates != o.UnreadUpdates) + return UnreadUpdates < o.UnreadUpdates; + return false; + } + DOCUMENT("The private ID of the bug report."); + rdcstr ID; + DOCUMENT("The original date when this bug was submitted."); + QDateTime SubmitDate; + DOCUMENT("The last date that we checked for updates."); + QDateTime CheckDate; + DOCUMENT("Unread updates to the bug exist"); + bool UnreadUpdates = false; + + DOCUMENT(R"(Gets the URL for this report. + +:return: The URL to the report. +:rtype: ``str`` +)"); + rdcstr URL() const { return lit(BUGREPORT_URL "/report/%1").arg(QString(ID)); } +}; + +DECLARE_REFLECTION_STRUCT(BugReport); + #define CONFIG_SETTING_VAL(access, variantType, type, name, defaultValue) \ access: \ type name = defaultValue; @@ -152,6 +196,16 @@ DECLARE_REFLECTION_STRUCT(SPIRVDisassembler); \ CONFIG_SETTING_VAL(public, bool, bool, Analytics_ManualCheck, false) \ \ + CONFIG_SETTING_VAL(public, bool, bool, CrashReport_EmailNagged, false) \ + \ + CONFIG_SETTING_VAL(public, bool, bool, CrashReport_ShouldRememberEmail, true) \ + \ + CONFIG_SETTING_VAL(public, QString, rdcstr, CrashReport_EmailAddress, "") \ + \ + CONFIG_SETTING_VAL(public, QString, rdcstr, CrashReport_LastOpenedCapture, "") \ + \ + CONFIG_SETTING(public, QVariantList, rdcarray, CrashReport_ReportedBugs) \ + \ CONFIG_SETTING(private, QVariantMap, rdcstrpairs, ConfigSettings) \ \ CONFIG_SETTING(private, QVariantList, rdcarray, RemoteHostList) @@ -460,6 +514,35 @@ For more information about some of these settings that are user-facing see Defaults to ``False``. +.. data:: CrashReport_EmailNagged + + ``True`` if the user has been prompted to enter their email address on a crash report. This really + helps find fixes for bugs, so we prompt the user once only if they didn't enter an email. Once the + prompt has happened, regardless of the answer this is set to true and remains there forever. + + Defaults to ``False``. + +.. data:: CrashReport_ShouldRememberEmail + + ``True`` if the email address entered in the crash reporter should be remembered for next time. If + no email is entered then nothing happens (any previous saved email is kept). + + Defaults to ``True``. + +.. data:: CrashReport_EmailAddress + + The saved email address for pre-filling out in crash reports. + +.. data:: CrashReport_LastOpenedCapture + + The last opened capture, to send if any crash is encountered. This is different to the most recent + opened file, because it's set before any processing happens (recent files are only added to the + list when they successfully open), and it's cleared again when the capture is closed. + +.. data:: CrashReport_ReportedBugs + + A list of :class:`BugReport` detailing previously submitted bugs that we're watching for updates. + )"); class PersistantConfig { diff --git a/qrenderdoc/Code/ReplayManager.cpp b/qrenderdoc/Code/ReplayManager.cpp index ecb5be947..a9145baee 100644 --- a/qrenderdoc/Code/ReplayManager.cpp +++ b/qrenderdoc/Code/ReplayManager.cpp @@ -33,10 +33,13 @@ ReplayManager::ReplayManager() { m_Running = false; m_Thread = NULL; + + RENDERDOC_RegisterMemoryRegion(this, sizeof(ReplayManager)); } ReplayManager::~ReplayManager() { + RENDERDOC_UnregisterMemoryRegion(this); } void ReplayManager::OpenCapture(const QString &capturefile, float *progress) diff --git a/qrenderdoc/Code/Resources.h b/qrenderdoc/Code/Resources.h index 55f53d754..7f43df255 100644 --- a/qrenderdoc/Code/Resources.h +++ b/qrenderdoc/Code/Resources.h @@ -36,6 +36,7 @@ RESOURCE_DEF(arrow_right, "arrow_right.png") \ RESOURCE_DEF(arrow_undo, "arrow_undo.png") \ RESOURCE_DEF(asterisk_orange, "asterisk_orange.png") \ + RESOURCE_DEF(bug, "bug.png") \ RESOURCE_DEF(chart_curve, "chart_curve.png") \ RESOURCE_DEF(cog, "cog.png") \ RESOURCE_DEF(color_wheel, "color_wheel.png") \ diff --git a/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i b/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i index efbca2fd4..5dd86e7d7 100644 --- a/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i +++ b/qrenderdoc/Code/pyrenderdoc/qrenderdoc.i @@ -15,7 +15,9 @@ %{ #define ENABLE_QT_CONVERT + #define RENDERDOC_QT_COMPAT + #include #include #include #include @@ -85,6 +87,7 @@ TEMPLATE_ARRAY_INSTANTIATE(rdcarray, VertexInputAttribute) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, BoundResource) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, BoundResourceArray) TEMPLATE_ARRAY_INSTANTIATE(rdcarray, rdcstrpair) +TEMPLATE_ARRAY_INSTANTIATE(rdcarray, BugReport) TEMPLATE_ARRAY_INSTANTIATE_PTR(rdcarray, ICaptureViewer) // unignore the function from above diff --git a/qrenderdoc/Code/qrenderdoc.cpp b/qrenderdoc/Code/qrenderdoc.cpp index 6ce2bb8c6..6578afffd 100644 --- a/qrenderdoc/Code/qrenderdoc.cpp +++ b/qrenderdoc/Code/qrenderdoc.cpp @@ -33,6 +33,7 @@ #include "Code/QRDUtils.h" #include "Code/Resources.h" #include "Code/pyrenderdoc/PythonContext.h" +#include "Windows/Dialogs/CrashDialog.h" #include "Windows/MainWindow.h" #include "version.h" @@ -150,6 +151,15 @@ int main(int argc, char *argv[]) } } + QString crashReportPath; + if(argc == 3 && !QString::compare(QString::fromUtf8(argv[1]), lit("--crash"), Qt::CaseInsensitive)) + { + crashReportPath = QString::fromUtf8(argv[2]); + + // 'consume' the report path so it doesn't get opened as a capture file + argc = 2; + } + QList pyscripts; for(int i = 0; i + 1 < argc; i++) @@ -226,17 +236,37 @@ int main(int argc, char *argv[]) GUIInvoke::init(); - PythonContext::GlobalInit(); - { GlobalEnvironment env; #if defined(RENDERDOC_PLATFORM_LINUX) env.xlibDisplay = QX11Info::display(); #endif - RENDERDOC_InitGlobalEnv(env, rdcarray()); + rdcarray args; + if(!crashReportPath.isEmpty()) + args.push_back("--crash"); + RENDERDOC_InitGlobalEnv(env, args); } + if(!crashReportPath.isEmpty()) { + QFile f(crashReportPath); + + if(f.exists() && f.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QVariantMap json = JSONToVariant(QString::fromUtf8(f.readAll())); + + if(json.contains(lit("report"))) + { + CrashDialog dialog(config, json); + + RDDialog::show(&dialog); + } + } + } + else + { + PythonContext::GlobalInit(); + CaptureContext ctx(filename, remoteHost, remoteIdent, temp, config); Analytics::Prompt(ctx, config); @@ -305,8 +335,9 @@ int main(int argc, char *argv[]) } config.Save(); + + PythonContext::GlobalShutdown(); } - PythonContext::GlobalShutdown(); Formatter::shutdown(); } diff --git a/qrenderdoc/Resources/bug.png b/qrenderdoc/Resources/bug.png new file mode 100644 index 000000000..c7299fd7d Binary files /dev/null and b/qrenderdoc/Resources/bug.png differ diff --git a/qrenderdoc/Resources/bug@2x.png b/qrenderdoc/Resources/bug@2x.png new file mode 100644 index 000000000..563c364c7 Binary files /dev/null and b/qrenderdoc/Resources/bug@2x.png differ diff --git a/qrenderdoc/Resources/resources.qrc b/qrenderdoc/Resources/resources.qrc index 9a16fa668..222300347 100644 --- a/qrenderdoc/Resources/resources.qrc +++ b/qrenderdoc/Resources/resources.qrc @@ -33,6 +33,8 @@ arrow_undo@2x.png asterisk_orange.png asterisk_orange@2x.png + bug.png + bug@2x.png chart_curve.png chart_curve@2x.png checkerboard.png diff --git a/qrenderdoc/Windows/Dialogs/CrashDialog.cpp b/qrenderdoc/Windows/Dialogs/CrashDialog.cpp new file mode 100644 index 000000000..a9ab90bd8 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/CrashDialog.cpp @@ -0,0 +1,463 @@ +/****************************************************************************** + * 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 "CrashDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Code/QRDUtils.h" +#include "ui_CrashDialog.h" + +CrashDialog::CrashDialog(PersistantConfig &cfg, QVariantMap crashReportJSON, QWidget *parent) + : QDialog(parent), ui(new Ui::CrashDialog), m_Config(cfg) +{ + ui->setupUi(this); + + m_NetManager = new QNetworkAccessManager(this); + + m_ReportPath = crashReportJSON[lit("report")].toString(); + m_ReportMetadata = crashReportJSON; + + bool replayCrash = crashReportJSON[lit("replaycrash")].toUInt() != 0; + + // remove metadata we don't send directly + m_ReportMetadata.remove(lit("report")); + m_ReportMetadata.remove(lit("replaycrash")); + + setStage(ReportStage::FillingDetails); + + m_CaptureFilename = m_Config.CrashReport_LastOpenedCapture; + + ui->rememberEmail->setChecked(m_Config.CrashReport_ShouldRememberEmail); + ui->email->setText(m_Config.CrashReport_EmailAddress); + + QFileInfo capInfo(m_CaptureFilename); + + if(replayCrash && capInfo.exists()) + { + // if we have a previous capture, fill out the capture group + ui->captureFilename->setText(capInfo.fileName()); + + // hide the preview until we have a successful thumbnail + ui->capturePreviewFrame->hide(); + + ICaptureFile *cap = RENDERDOC_OpenCaptureFile(); + + ReplayStatus status = cap->OpenFile(capInfo.absoluteFilePath().toUtf8().data(), ""); + + if(status == ReplayStatus::Succeeded) + { + Thumbnail thumb = cap->GetThumbnail(FileType::Raw, 320); + QImage i = QImage(thumb.data.data(), (int)thumb.width, (int)thumb.height, QImage::Format_RGB888) + .copy(0, 0, (int)thumb.width, (int)thumb.height); + if(!i.isNull()) + { + ui->capturePreview->setPixmap(QPixmap::fromImage(i)); + ui->capturePreview->setPreserveAspectRatio(true); + ui->capturePreviewFrame->show(); + + m_Thumbnail = new Thumbnail(cap->GetThumbnail(FileType::JPG, 0)); + } + } + + cap->Shutdown(); + } + else + { + m_CaptureFilename = QString(); + + // otherwise hide it entirely - this is probably a crash in the injected application or + // something along those lines where a capture isn't directly associated. + ui->captureLabel->hide(); + ui->captureUpload->hide(); + ui->captureFilename->hide(); + ui->capturePreviewFrame->hide(); + } + + QString text = + tr("

RenderDoc encountered a serious problem. Please take a moment to look over this " + "form and send it off so that RenderDoc can get better!

"); + + text += tr("

The contents of the report can be found in this zip which " + "you can edit/censor if you wish.

") + .arg(QUrl::fromLocalFile(m_ReportPath).toString()); + + text += tr("

More information about the bug " + "reporter and privacy statement " + "for submissions."); + + ui->reportText->setTextFormat(Qt::RichText); + ui->reportText->setText(text); + + setWindowFlags((windowFlags() | Qt::MSWindowsFixedSizeDialogHint) & + ~Qt::WindowContextHelpButtonHint); + + adjustSize(); +} + +CrashDialog::~CrashDialog() +{ + delete m_UploadTimer; + delete m_Thumbnail; + + delete ui; +} + +void CrashDialog::showEvent(QShowEvent *) +{ + adjustSize(); + recentre(); +} + +void CrashDialog::resizeEvent(QResizeEvent *) +{ + recentre(); +} + +void CrashDialog::recentre() +{ + QRect scr = QApplication::desktop()->screenGeometry(); + move(scr.center() - rect().center()); + + // when we're first shown, on this stage, move the cursor + if(m_Stage == ReportStage::FillingDetails) + QCursor::setPos(geometry().center()); +} + +void CrashDialog::setStage(ReportStage stage) +{ + m_Stage = stage; + + switch(stage) + { + case ReportStage::FillingDetails: + ui->reportGroup->show(); + ui->uploadingGroup->hide(); + ui->reportedGroup->hide(); + break; + case ReportStage::Uploading: + ui->reportGroup->hide(); + ui->uploadingGroup->show(); + ui->reportedGroup->hide(); + break; + case ReportStage::Reported: + ui->reportGroup->hide(); + ui->uploadingGroup->hide(); + ui->reportedGroup->show(); + break; + } + + adjustSize(); +} + +void CrashDialog::on_send_clicked() +{ + // confirm if the user REALLY wants to upload their capture + 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?")); + + if(result != QMessageBox::Yes) + { + // uncheck and return back so they can confirm + ui->captureUpload->setChecked(false); + return; + } + } + + // if we haven't nagged the user before about entering their email address, do so now. + if(!m_Config.CrashReport_EmailNagged && ui->email->text().isEmpty()) + { + // don't prompt about this again + m_Config.CrashReport_EmailNagged = true; + m_Config.Save(); + + QMessageBox::StandardButton result = + RDDialog::question(this, tr("Please consider leaving your email"), + tr("Most bug reports without an email address for contact can't be " + "resolved. Would you like to enter your email address?\n\n" + "You won't be asked about this again.")); + + if(result == QMessageBox::Yes) + { + // focus the email field and return so the user can enter something + ui->email->setFocus(Qt::OtherFocusReason); + return; + } + } + + // save the email configuration for next time so the user can click-through. + m_Config.CrashReport_ShouldRememberEmail = ui->rememberEmail->isChecked(); + if(ui->rememberEmail->isChecked() && !ui->email->text().isEmpty()) + m_Config.CrashReport_EmailAddress = ui->email->text(); + m_Config.Save(); + + sendReport(); + + setStage(ReportStage::Uploading); +} + +void CrashDialog::sendReport() +{ + delete m_Request; + m_Request = NULL; + + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + // from the QHttpMultiPart example + for(QString key : m_ReportMetadata.keys()) + { + QHttpPart param; + param.setHeader(QNetworkRequest::ContentDispositionHeader, + lit("form-data; name=\"%1\"").arg(key)); + param.setBody(m_ReportMetadata[key].toString().toUtf8()); + + multiPart->append(param); + } + + QString email = ui->email->text(); + QString description = ui->description->toPlainText(); + + if(!email.isEmpty()) + { + QHttpPart param; + param.setHeader(QNetworkRequest::ContentDispositionHeader, lit("form-data; name=\"email\"")); + param.setBody(email.toUtf8()); + + multiPart->append(param); + } + + if(!description.isEmpty()) + { + QHttpPart param; + param.setHeader(QNetworkRequest::ContentDispositionHeader, + lit("form-data; name=\"description\"")); + param.setBody(description.toUtf8()); + + multiPart->append(param); + } + + if(!m_CaptureFilename.isEmpty() && ui->captureUpload->isChecked()) + { + { + QHttpPart capture; + + QFile *file = new QFile(m_CaptureFilename); + file->open(QIODevice::ReadOnly); + file->setParent(multiPart); + + capture.setHeader(QNetworkRequest::ContentTypeHeader, lit("application/x-renderdoc-capture")); + capture.setHeader(QNetworkRequest::ContentDispositionHeader, + lit("form-data; name=\"capture\"; filename=\"capture.rdc\"")); + capture.setBodyDevice(file); + + multiPart->append(capture); + } + + if(m_Thumbnail) + { + QHttpPart capture; + + QByteArray thumb; + + thumb.insert(0, (const char *)m_Thumbnail->data.data(), m_Thumbnail->data.count()); + + capture.setHeader(QNetworkRequest::ContentTypeHeader, lit("image/jpeg")); + capture.setHeader(QNetworkRequest::ContentDispositionHeader, + lit("form-data; name=\"thumb\"; filename=\"thumb.jpg\"")); + capture.setBody(thumb); + + multiPart->append(capture); + } + } + + { + QHttpPart report; + + QFile *file = new QFile(m_ReportPath); + file->open(QIODevice::ReadOnly); + file->setParent(multiPart); + + report.setHeader(QNetworkRequest::ContentTypeHeader, lit("application/zip")); + report.setHeader(QNetworkRequest::ContentDispositionHeader, + lit("form-data; name=\"report\"; filename=\"report.zip\"")); + report.setBodyDevice(file); + + multiPart->append(report); + } + + QNetworkRequest request(QUrl(lit(BUGREPORT_URL))); + + m_Request = m_NetManager->post(request, multiPart); + multiPart->setParent(m_Request); + + QObject::connect( + m_Request, OverloadedSlot::of(&QNetworkReply::error), + [this](QNetworkReply::NetworkError err) { + ui->progressBar->setValue(0); + ui->progressText->setText(tr("Network error uploading:\n%1").arg(m_Request->errorString())); + ui->uploadRetry->setEnabled(true); + }); + + ui->progressBar->setMaximum(10000); + ui->progressBar->setValue(0); + ui->progressText->setText(tr("Uploading report...\nCalculating time remaining")); + + delete m_UploadTimer; + m_UploadTimer = new QElapsedTimer(); + + 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)); + } + } + }); + + QObject::connect(m_Request, &QNetworkReply::finished, [this]() { + + // don't do anything if we're finished after an error + if(ui->uploadRetry->isEnabled()) + return; + + QString text = tr("

Your report has been uploaded, thank you for your help!

"); + + m_ReportID = QString::fromUtf8(m_Request->readAll()); + + if(!m_ReportID.isEmpty()) + { + BugReport bug; + bug.ID = m_ReportID; + QString url = bug.URL(); + + text += + tr("

The unique anonymous URL for your report is %1.

").arg(url); + } + + ui->finishedText->setTextFormat(Qt::RichText); + ui->finishedText->setText(text); + setStage(ReportStage::Reported); + }); +} + +void CrashDialog::on_cancel_clicked() +{ + // don't nag the user, just close. + reject(); +} + +void CrashDialog::on_uploadCancel_clicked() +{ + // check that it wasn't an accident + QMessageBox::StandardButton result = RDDialog::question( + this, tr("Cancel upload?"), tr("Are you sure you want to cancel the bug report upload?")); + + if(result == QMessageBox::Yes) + { + // cancel the request in flight + m_Request->abort(); + delete m_Request; + + // then close the window + reject(); + } +} + +void CrashDialog::on_uploadRetry_clicked() +{ + // restart the request + sendReport(); + ui->uploadRetry->setEnabled(false); +} + +void CrashDialog::on_buttonBox_accepted() +{ + if(!m_ReportID.isEmpty() && ui->checkUpdates->isChecked()) + { + // add to list of bug reports to check for updates. + BugReport bug; + bug.ID = m_ReportID; + bug.SubmitDate = QDateTime::currentDateTimeUtc(); + bug.CheckDate = QDateTime::currentDateTimeUtc(); + m_Config.CrashReport_ReportedBugs.push_back(bug); + + if(m_Config.CrashReport_ReportedBugs.count() > 20) + m_Config.CrashReport_ReportedBugs.erase(0); + + m_Config.Save(); + } + + accept(); +} diff --git a/qrenderdoc/Windows/Dialogs/CrashDialog.h b/qrenderdoc/Windows/Dialogs/CrashDialog.h new file mode 100644 index 000000000..b36d6ec10 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/CrashDialog.h @@ -0,0 +1,90 @@ +/****************************************************************************** + * 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 +#include + +namespace Ui +{ +class CrashDialog; +} + +class PersistantConfig; +class QNetworkAccessManager; +class QNetworkReply; +class QElapsedTimer; + +struct Thumbnail; + +class CrashDialog : public QDialog +{ + Q_OBJECT +public: + explicit CrashDialog(PersistantConfig &cfg, QVariantMap crashReportJSON, QWidget *parent = 0); + ~CrashDialog(); + +private slots: + // automatic slots + void on_send_clicked(); + + void sendReport(); + + void on_cancel_clicked(); + void on_uploadCancel_clicked(); + void on_uploadRetry_clicked(); + void on_buttonBox_accepted(); + +private: + void showEvent(QShowEvent *) override; + void resizeEvent(QResizeEvent *) override; + + enum class ReportStage + { + FillingDetails, + Uploading, + Reported, + }; + + void recentre(); + void setStage(ReportStage stage); + + Ui::CrashDialog *ui; + + ReportStage m_Stage; + QString m_CaptureFilename; + QString m_ReportPath; + QString m_ReportID; + QVariantMap m_ReportMetadata; + + QElapsedTimer *m_UploadTimer = NULL; + + QNetworkAccessManager *m_NetManager; + QNetworkReply *m_Request = NULL; + + Thumbnail *m_Thumbnail = NULL; + + PersistantConfig &m_Config; +}; diff --git a/qrenderdoc/Windows/Dialogs/CrashDialog.ui b/qrenderdoc/Windows/Dialogs/CrashDialog.ui new file mode 100644 index 000000000..69ebdb4df --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/CrashDialog.ui @@ -0,0 +1,565 @@ + + + CrashDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 593 + 1004 + + + + + 0 + 0 + + + + RenderDoc Bug Reporter + + + + 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 + + + + RenderDoc Bug Reporter + + + + + + + + + + 0 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + Bug Report + + + + + + QFrame::Box + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 320 + 180 + + + + + 320 + 180 + + + + true + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Send + + + + + + + + + + Remember email address for next time + + + + + + + filename_of_capture.rdc + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + + + + Filled out at runtime + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + + + Email Address + + + + + + + Last Capture + + + + + + + Upload this capture with bug report + + + + + + + Enter your contact info to help fix this bug + + + + + + + Description of Problem + + + true + + + + + + + Qt::Horizontal + + + + + + + + + + Uploading + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Uploading Bug Report + + + true + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Retry + + + + + + + Cancel + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Reported + + + + + + Filled out at runtime + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + + + Check for updates to this bug report + + + true + + + + + + + QDialogButtonBox::Ok + + + + + + + + + + + + + RDLabel + QLabel +
Widgets/Extended/RDLabel.h
+
+
+ + + + +
diff --git a/qrenderdoc/Windows/MainWindow.cpp b/qrenderdoc/Windows/MainWindow.cpp index b94f9b3f0..a42ed0a5c 100644 --- a/qrenderdoc/Windows/MainWindow.cpp +++ b/qrenderdoc/Windows/MainWindow.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,6 +41,7 @@ #include "Widgets/Extended/RDLabel.h" #include "Windows/Dialogs/AboutDialog.h" #include "Windows/Dialogs/CaptureDialog.h" +#include "Windows/Dialogs/CrashDialog.h" #include "Windows/Dialogs/LiveCapture.h" #include "Windows/Dialogs/RemoteManager.h" #include "Windows/Dialogs/SettingsDialog.h" @@ -171,8 +174,79 @@ MainWindow::MainWindow(ICaptureContext &ctx) : QMainWindow(NULL), ui(new Ui::Mai SetTitle(); +#if defined(RELEASE) + ui->action_Send_Error_Report->setEnabled(true); +#else + ui->action_Send_Error_Report->setEnabled(false); +#endif + PopulateRecentCaptureFiles(); PopulateRecentCaptureSettings(); + PopulateReportedBugs(); + + m_NetManager = new QNetworkAccessManager(this); + + rdcarray bugs = m_Ctx.Config().CrashReport_ReportedBugs; + LambdaThread *bugupdate = new LambdaThread([this, bugs]() { + QDateTime now = QDateTime::currentDateTimeUtc(); + + // loop over all the bugs + for(const BugReport &b : bugs) + { + // check bugs every two days + qint64 diff = b.CheckDate.secsTo(now); + if(diff > 2 * 24 * 60 * 60) + { + // update the check date on the stored bug + GUIInvoke::call([this, b, now]() { + for(BugReport &bug : m_Ctx.Config().CrashReport_ReportedBugs) + { + if(bug.ID == b.ID) + { + bug.CheckDate = now; + break; + } + } + m_Ctx.Config().Save(); + + // call out to the status-check to see when the bug report was last updated + QNetworkReply *reply = + m_NetManager->get(QNetworkRequest(QUrl(QString(b.URL()) + lit("/check")))); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply, b]() { + QString response = QString::fromUtf8(reply->readAll()); + + if(response.isEmpty()) + return; + + // only look at the first line of the response + int idx = response.indexOf(QLatin1Char('\n')); + + if(idx > 0) + response.truncate(idx); + + QDateTime update = QDateTime::fromString(response, lit("yyyy-MM-dd HH:mm:ss")); + + // if there's been an update since the last check, set unread + if(update.isValid() && update > b.CheckDate) + { + for(BugReport &bug : m_Ctx.Config().CrashReport_ReportedBugs) + { + if(bug.ID == b.ID) + { + bug.UnreadUpdates = true; + break; + } + } + PopulateReportedBugs(); + } + }); + }); + } + } + }); + bugupdate->selfDelete(true); + bugupdate->start(); ui->toolWindowManager->setToolWindowCreateCallback([this](const QString &objectName) -> QWidget * { return m_Ctx.CreateBuiltinWindow(objectName); @@ -834,6 +908,60 @@ void MainWindow::PopulateRecentCaptureSettings() ui->menu_Recent_Capture_Settings->addAction(ui->action_Clear_Capture_Settings_History); } +void MainWindow::PopulateReportedBugs() +{ + ui->menu_Reported_Bugs->clear(); + + ui->menu_Reported_Bugs->setEnabled(false); + + bool unread = false; + + int idx = 1; + for(int i = m_Ctx.Config().CrashReport_ReportedBugs.count() - 1; i >= 0; i--) + { + BugReport &bug = m_Ctx.Config().CrashReport_ReportedBugs[i]; + QString fmt = tr("&%1: Bug reported at %2"); + + if(bug.UnreadUpdates) + fmt = tr("&%1: (Update) Bug reported at %2"); + + QAction *action = + ui->menu_Reported_Bugs->addAction(fmt.arg(idx).arg(bug.SubmitDate.toString()), [this, i] { + BugReport &bug = m_Ctx.Config().CrashReport_ReportedBugs[i]; + + QDesktopServices::openUrl(QString(bug.URL())); + + bug.UnreadUpdates = false; + m_Ctx.Config().Save(); + + PopulateReportedBugs(); + }); + idx++; + + if(bug.UnreadUpdates) + { + action->setIcon(Icons::bug()); + unread = true; + } + + ui->menu_Reported_Bugs->setEnabled(true); + } + + ui->menu_Reported_Bugs->addSeparator(); + ui->menu_Reported_Bugs->addAction(ui->action_Clear_Reported_Bugs); + + if(unread) + { + ui->menu_Help->setIcon(Icons::bug()); + ui->menu_Reported_Bugs->setIcon(Icons::bug()); + } + else + { + ui->menu_Help->setIcon(QIcon()); + ui->menu_Reported_Bugs->setIcon(QIcon()); + } +} + void MainWindow::ShowLiveCapture(LiveCapture *live) { m_LiveCaptures.push_back(live); @@ -1822,6 +1950,28 @@ void MainWindow::on_action_Resource_Inspector_triggered() ui->toolWindowManager->addToolWindow(resourceInspector, mainToolArea()); } +void MainWindow::on_action_Send_Error_Report_triggered() +{ + rdcstr report; + RENDERDOC_CreateBugReport(RENDERDOC_GetLogFile(), "", report); + + QVariantMap json; + + json[lit("version")] = lit(FULL_VERSION_STRING); + json[lit("gitcommit")] = lit(GIT_COMMIT_HASH); + json[lit("replaycrash")] = 1; + json[lit("report")] = (QString)report; + + CrashDialog crash(m_Ctx.Config(), json, this); + + RDDialog::show(&crash); + + m_Ctx.Config().Save(); + PopulateReportedBugs(); + + QFile::remove(QString(report)); +} + void MainWindow::saveLayout_triggered() { LoadSaveLayout(qobject_cast(QObject::sender()), true); diff --git a/qrenderdoc/Windows/MainWindow.h b/qrenderdoc/Windows/MainWindow.h index f4d0322c6..adea64158 100644 --- a/qrenderdoc/Windows/MainWindow.h +++ b/qrenderdoc/Windows/MainWindow.h @@ -43,6 +43,7 @@ class QProgressBar; class QToolButton; class CaptureDialog; class LiveCapture; +class QNetworkAccessManager; class MainWindow : public QMainWindow, public IMainWindow, public ICaptureViewer { @@ -102,6 +103,7 @@ public: void showResourceInspector() { on_action_Resource_Inspector_triggered(); } void PopulateRecentCaptureFiles(); void PopulateRecentCaptureSettings(); + void PopulateReportedBugs(); private slots: // automatic slots void on_action_Exit_triggered(); @@ -135,6 +137,7 @@ private slots: void on_action_Show_Tips_triggered(); void on_action_Counter_Viewer_triggered(); void on_action_Resource_Inspector_triggered(); + void on_action_Send_Error_Report_triggered(); // manual slots void saveLayout_triggered(); @@ -177,6 +180,8 @@ private: QSemaphore m_RemoteProbeSemaphore; LambdaThread *m_RemoteProbe; + QNetworkAccessManager *m_NetManager; + bool m_messageAlternate = false; bool m_OwnTempCapture = false; diff --git a/qrenderdoc/Windows/MainWindow.ui b/qrenderdoc/Windows/MainWindow.ui index feba1eb29..b7a327b2a 100644 --- a/qrenderdoc/Windows/MainWindow.ui +++ b/qrenderdoc/Windows/MainWindow.ui @@ -140,11 +140,19 @@ &Help + + + &Reported Bugs + + + + + @@ -430,6 +438,11 @@ Re&compress Capture + + + &Clear Reported Bugs + + diff --git a/qrenderdoc/qrenderdoc.pro b/qrenderdoc/qrenderdoc.pro index a9c68c003..e8e9bc4c3 100644 --- a/qrenderdoc/qrenderdoc.pro +++ b/qrenderdoc/qrenderdoc.pro @@ -171,6 +171,7 @@ SOURCES += Code/qrenderdoc.cpp \ Styles/RDStyle/RDStyle.cpp \ Styles/RDTweakedNativeStyle/RDTweakedNativeStyle.cpp \ Windows/Dialogs/AboutDialog.cpp \ + Windows/Dialogs/CrashDialog.cpp \ Windows/MainWindow.cpp \ Windows/EventBrowser.cpp \ Windows/TextureViewer.cpp \ @@ -242,6 +243,7 @@ HEADERS += Code/CaptureContext.h \ Styles/RDStyle/RDStyle.h \ Styles/RDTweakedNativeStyle/RDTweakedNativeStyle.h \ Windows/Dialogs/AboutDialog.h \ + Windows/Dialogs/CrashDialog.h \ Windows/MainWindow.h \ Windows/EventBrowser.h \ Windows/TextureViewer.h \ @@ -296,6 +298,7 @@ HEADERS += Code/CaptureContext.h \ Windows/Dialogs/AnalyticsConfirmDialog.h \ Windows/Dialogs/AnalyticsPromptDialog.h FORMS += Windows/Dialogs/AboutDialog.ui \ + Windows/Dialogs/CrashDialog.ui \ Windows/MainWindow.ui \ Windows/EventBrowser.ui \ Windows/TextureViewer.ui \ diff --git a/qrenderdoc/qrenderdoc_local.vcxproj b/qrenderdoc/qrenderdoc_local.vcxproj index a0bdccb0b..d16571e14 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj +++ b/qrenderdoc/qrenderdoc_local.vcxproj @@ -577,6 +577,7 @@ + @@ -688,6 +689,7 @@ + @@ -885,6 +887,7 @@ + @@ -1119,6 +1122,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" @@ -1359,6 +1368,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" @@ -1519,42 +1534,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resources\resources.qrc;$(ProjectDir)3rdparty\qt\$(Platform)\bin\rcc.exe;%(AdditionalInputs) "$(ProjectDir)3rdparty\qt\$(Platform)\bin\rcc.exe" -name resources Resources\resources.qrc -o "$(IntDir)generated\qrc_resources.cpp" @@ -1573,31 +1552,6 @@ IF %ERRORLEVEL% NEQ 0 (echo ==================================================== RCC qtconf.qrc $(IntDir)generated\qrc_qtconf.cpp - - - - - - - - - - - - - - - - - - - - - - - - - %(Fullpath);$(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe;%(AdditionalInputs) $(ProjectDir)3rdparty\qt\$(Platform)\bin\uic.exe %(Fullpath) -o $(IntDir)generated\ui_%(Filename).h @@ -1605,6 +1559,9 @@ IF %ERRORLEVEL% NEQ 0 (echo ==================================================== $(IntDir)generated\ui_%(Filename).h Designer + + + @@ -1743,6 +1700,119 @@ IF %ERRORLEVEL% NEQ 0 (echo ==================================================== RENDERDOC_PY_PATH=.\..\$(SolutionRelativeIntDir)\generated\renderdoc.py;QRENDERDOC_PY_PATH=.\..\$(SolutionRelativeIntDir)\generated\qrenderdoc.py;%(PreprocessorDefinitions) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qrenderdoc/qrenderdoc_local.vcxproj.filters b/qrenderdoc/qrenderdoc_local.vcxproj.filters index 9b780c817..b79cd3541 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj.filters +++ b/qrenderdoc/qrenderdoc_local.vcxproj.filters @@ -7,9 +7,6 @@ {D9D6E242-F8AF-46E4-B9FD-80ECBC20BA3E} - - {c6877252-7f18-4c63-a2f7-66b1913d8ada} - {476acc91-c8c7-4ba7-9835-c0b566f562dd} @@ -82,6 +79,9 @@ {c0be5204-4ee0-4948-87b4-cc957b6f8953} + + {c6877252-7f18-4c63-a2f7-66b1913d8ada} + @@ -690,6 +690,12 @@ Code\Interface + + Generated Files + + + Windows\Dialogs + @@ -1040,191 +1046,11 @@ Generated Files + + Generated Files + - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - - - Resources\Files - Code\pyrenderdoc @@ -1240,6 +1066,15 @@ Code\pyrenderdoc + + Resources\Files + + + Resources\Files + + + Resources\Files + @@ -1574,5 +1409,342 @@ Windows\Dialogs + + Windows\Dialogs + + + Windows\Dialogs + + + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + Resources\Files + + + + + Resources\Files + + + Resources\Files + \ No newline at end of file diff --git a/renderdoc/api/replay/renderdoc_replay.h b/renderdoc/api/replay/renderdoc_replay.h index 2c565b682..2ebfb4103 100644 --- a/renderdoc/api/replay/renderdoc_replay.h +++ b/renderdoc/api/replay/renderdoc_replay.h @@ -1880,9 +1880,16 @@ DOCUMENT("Internal function for initialising global process environment in a rep extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_InitGlobalEnv(GlobalEnvironment env, const rdcarray &args); -DOCUMENT("Internal function for triggering exception handler."); -extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_TriggerExceptionHandler(void *exceptionPtrs, - bool crashed); +DOCUMENT("Internal function for creating a bug report zip."); +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CreateBugReport(const char *logfile, + const char *dumpfile, + rdcstr &report); + +DOCUMENT("Internal function for registering a memory region to be saved with crash dumps."); +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_RegisterMemoryRegion(void *base, size_t size); + +DOCUMENT("Internal function for unregistering a memory region to be saved with crash dumps."); +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UnregisterMemoryRegion(void *base); DOCUMENT(R"(Sets the location for the diagnostic log output, shared by captured programs and the analysis program. diff --git a/renderdoc/core/crash_handler.h b/renderdoc/core/crash_handler.h index e85858fd7..f093b677a 100644 --- a/renderdoc/core/crash_handler.h +++ b/renderdoc/core/crash_handler.h @@ -24,7 +24,7 @@ ******************************************************************************/ // currently breakpad crash-handler is only available on windows -#if ENABLED(RDOC_RELEASE) && RENDERDOC_OFFICIAL_BUILD && ENABLED(RDOC_WIN32) +#if ENABLED(RDOC_RELEASE) && ENABLED(RDOC_WIN32) && RENDERDOC_OFFICIAL_BUILD #define RDOC_CRASH_HANDLER OPTION_ON @@ -107,9 +107,11 @@ public: google_breakpad::CustomInfoEntry(L"version", L""), google_breakpad::CustomInfoEntry(L"logpath", L""), google_breakpad::CustomInfoEntry(L"gitcommit", L""), + google_breakpad::CustomInfoEntry(L"replaycrash", + RenderDoc::Inst().IsReplayApp() ? L"1" : L"0"), }; - wstring wideStr = StringFormat::UTF82Wide(string(MAJOR_MINOR_VERSION_STRING)); + wstring wideStr = StringFormat::UTF82Wide(string(FULL_VERSION_STRING)); breakpadCustomInfo[0].set_value(wideStr.c_str()); wideStr = StringFormat::UTF82Wide(string(RDCGETLOGFILE())); breakpadCustomInfo[1].set_value(wideStr.c_str()); diff --git a/renderdoc/replay/entry_points.cpp b/renderdoc/replay/entry_points.cpp index ee877ec08..c4de46aea 100644 --- a/renderdoc/replay/entry_points.cpp +++ b/renderdoc/replay/entry_points.cpp @@ -31,6 +31,7 @@ #include "core/core.h" #include "maths/camera.h" #include "maths/formatpacking.h" +#include "miniz/miniz.h" #include "strings/string_utils.h" // these entry points are for the replay/analysis side - not for the application. @@ -237,32 +238,62 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_InitGlobalEnv(GlobalEnviron argsVec.push_back(a.c_str()); RenderDoc::Inst().ProcessGlobalEnvironment(env, argsVec); -} -extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_TriggerExceptionHandler(void *exceptionPtrs, - bool crashed) -{ if(RenderDoc::Inst().GetCrashHandler() == NULL) return; - if(exceptionPtrs) + for(const rdcstr &s : args) { - RenderDoc::Inst().GetCrashHandler()->WriteMinidump(exceptionPtrs); - } - else - { - if(!crashed) + if(s == "--crash") { - RDCLOG("Writing crash log"); - } - - RenderDoc::Inst().GetCrashHandler()->WriteMinidump(); - - if(!crashed) - { - RenderDoc::Inst().RecreateCrashHandler(); + RenderDoc::Inst().UnloadCrashHandler(); + return; } } + + RenderDoc::Inst().RecreateCrashHandler(); +} + +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CreateBugReport(const char *logfile, + const char *dumpfile, + rdcstr &report) +{ + mz_zip_archive zip; + RDCEraseEl(zip); + + report = FileIO::GetTempFolderFilename() + "/renderdoc_report.zip"; + + FileIO::Delete(report.c_str()); + + mz_zip_writer_init_file(&zip, report.c_str(), 0); + + if(dumpfile && dumpfile[0]) + mz_zip_writer_add_file(&zip, "minidump.dmp", dumpfile, NULL, 0, MZ_BEST_COMPRESSION); + + if(logfile && logfile[0]) + { + std::string contents = FileIO::logfile_readall(logfile); + mz_zip_writer_add_mem(&zip, "error.log", contents.data(), contents.length(), MZ_BEST_COMPRESSION); + } + + mz_zip_writer_finalize_archive(&zip); + mz_zip_writer_end(&zip); +} + +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_RegisterMemoryRegion(void *base, size_t size) +{ + ICrashHandler *handler = RenderDoc::Inst().GetCrashHandler(); + + if(handler) + handler->RegisterMemoryRegion(base, size); +} + +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_UnregisterMemoryRegion(void *base) +{ + ICrashHandler *handler = RenderDoc::Inst().GetCrashHandler(); + + if(handler) + handler->UnregisterMemoryRegion(base); } extern "C" RENDERDOC_API uint32_t RENDERDOC_CC diff --git a/renderdoccmd/renderdoccmd.rc b/renderdoccmd/renderdoccmd.rc index 4152f8a49..3a4125ebb 100644 Binary files a/renderdoccmd/renderdoccmd.rc and b/renderdoccmd/renderdoccmd.rc differ diff --git a/renderdoccmd/renderdoccmd_win32.cpp b/renderdoccmd/renderdoccmd_win32.cpp index c697c3df0..40a5f4d8f 100644 --- a/renderdoccmd/renderdoccmd_win32.cpp +++ b/renderdoccmd/renderdoccmd_win32.cpp @@ -67,6 +67,12 @@ static std::wstring conv(const std::string &str) HINSTANCE hInstance = NULL; #if defined(RELEASE) +#define CRASH_HANDLER 1 +#else +#define CRASH_HANDLER 0 +#endif + +#if CRASH_HANDLER // breakpad #include "breakpad/client/windows/crash_generation/client_info.h" #include "breakpad/client/windows/crash_generation/crash_generation_server.h" @@ -77,150 +83,15 @@ using google_breakpad::CrashGenerationServer; bool exitServer = false; -static HINSTANCE CrashHandlerInst = 0; -static HWND CrashHandlerWnd = 0; - -bool uploadReport = false; -bool uploadDump = false; -bool uploadLog = false; -string reproSteps = ""; - -wstring dump = L""; -vector customInfo; -wstring logpath = L""; - -INT_PTR CALLBACK CrashHandlerProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch(message) - { - case WM_INITDIALOG: - { - HANDLE hIcon = LoadImage(CrashHandlerInst, MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 16, 16, 0); - - if(hIcon) - { - SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); - SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); - } - - SetDlgItemTextW( - hDlg, IDC_WELCOMETEXT, - L"RenderDoc has encountered an unhandled exception or other similar unrecoverable " - L"error.\n\n" - L"If you had captured but not saved a logfile it should still be available in %TEMP% and " - L"will not be deleted," - L"you can try loading it again.\n\n" - L"A minidump has been created and the RenderDoc diagnostic log (NOT any capture logfile) " - L"is available if you would like " - L"to send them back to be analysed. The path for both is found below if you would like " - L"to inspect their contents and censor as appropriate.\n\n" - L"Neither contains any significant private information, the minidump has some internal " - L"states and local memory at the time of the " - L"crash & thread stacks, etc. The diagnostic log contains diagnostic messages like " - L"warnings and errors.\n\n" - L"The only other information sent is the version of RenderDoc, " - L"and any notes you include.\n\n" - L"Any repro steps or notes would be helpful to include with the report. If you'd like to " - L"be contacted about the bug " - L"e.g. for updates about its status just include your email & name. Thank you!\n\n" - L"Baldur (baldurk@baldurk.org)"); - - SetDlgItemTextW(hDlg, IDC_DUMPPATH, dump.c_str()); - SetDlgItemTextW(hDlg, IDC_LOGPATH, logpath.c_str()); - - CheckDlgButton(hDlg, IDC_SENDDUMP, BST_CHECKED); - CheckDlgButton(hDlg, IDC_SENDLOG, BST_CHECKED); - - { - RECT r; - GetClientRect(hDlg, &r); - - int xPos = (GetSystemMetrics(SM_CXSCREEN) - r.right) / 2; - int yPos = (GetSystemMetrics(SM_CYSCREEN) - r.bottom) / 2; - - SetWindowPos(hDlg, HWND_TOPMOST, xPos, yPos, 0, 0, SWP_NOSIZE); - } - - return (INT_PTR)TRUE; - } - - case WM_SHOWWINDOW: - { - { - RECT r; - GetClientRect(hDlg, &r); - - int xPos = (GetSystemMetrics(SM_CXSCREEN) - r.right) / 2; - int yPos = (GetSystemMetrics(SM_CYSCREEN) - r.bottom) / 2; - - SetWindowPos(hDlg, HWND_NOTOPMOST, xPos, yPos, 0, 0, SWP_NOSIZE); - } - - return (INT_PTR)TRUE; - } - - case WM_COMMAND: - { - int ID = LOWORD(wParam); - - if(ID == IDC_DONTSEND) - { - EndDialog(hDlg, 0); - return (INT_PTR)TRUE; - } - else if(ID == IDC_SEND) - { - uploadReport = true; - uploadDump = (IsDlgButtonChecked(hDlg, IDC_SENDDUMP) != 0); - uploadLog = (IsDlgButtonChecked(hDlg, IDC_SENDLOG) != 0); - - char notes[4097] = {0}; - - GetDlgItemTextA(hDlg, IDC_NAME, notes, 4096); - notes[4096] = 0; - - reproSteps = "Name: "; - reproSteps += notes; - reproSteps += "\n"; - - memset(notes, 0, 4096); - GetDlgItemTextA(hDlg, IDC_EMAIL, notes, 4096); - notes[4096] = 0; - - reproSteps += "Email: "; - reproSteps += notes; - reproSteps += "\n\n"; - - memset(notes, 0, 4096); - GetDlgItemTextA(hDlg, IDC_REPRO, notes, 4096); - notes[4096] = 0; - - reproSteps += notes; - - EndDialog(hDlg, 0); - return (INT_PTR)TRUE; - } - } - break; - - case WM_QUIT: - case WM_DESTROY: - case WM_CLOSE: - { - EndDialog(hDlg, 0); - return (INT_PTR)TRUE; - } - break; - } - return (INT_PTR)FALSE; -} +wstring wdump = L""; +std::vector customInfo; static void _cdecl OnClientCrashed(void *context, const ClientInfo *client_info, const wstring *dump_path) { if(dump_path) { - dump = *dump_path; + wdump = *dump_path; google_breakpad::CustomClientInfo custom = client_info->GetCustomInfo(); @@ -498,7 +369,7 @@ struct UpgradeCommand : public Command } }; -#if defined(RELEASE) +#if CRASH_HANDLER struct CrashHandlerCommand : public Command { CrashHandlerCommand(const GlobalEnvironment &env) : Command(env) {} @@ -531,22 +402,6 @@ struct CrashHandlerCommand : public Command return 1; } - CrashHandlerInst = hInstance; - - CrashHandlerWnd = - CreateWindowEx(WS_EX_CLIENTEDGE, L"renderdoccmd", L"renderdoccmd", WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, 10, 10, NULL, NULL, hInstance, NULL); - - HANDLE hIcon = LoadImage(CrashHandlerInst, MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 16, 16, 0); - - if(hIcon) - { - SendMessage(CrashHandlerWnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); - SendMessage(CrashHandlerWnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon); - } - - ShowWindow(CrashHandlerWnd, SW_HIDE); - HANDLE readyEvent = CreateEventA(NULL, TRUE, FALSE, "RENDERDOC_CRASHHANDLE"); if(readyEvent != NULL) @@ -578,11 +433,11 @@ struct CrashHandlerCommand : public Command delete crashServer; crashServer = NULL; - if(!dump.empty()) - { - logpath = L""; + std::wstring wlogpath; - string report = ""; + if(!wdump.empty()) + { + string report = "{\n"; for(size_t i = 0; i < customInfo.size(); i++) { @@ -591,7 +446,7 @@ struct CrashHandlerCommand : public Command if(name == L"logpath") { - logpath = val; + wlogpath = val; } else if(name == L"ptime") { @@ -599,56 +454,77 @@ struct CrashHandlerCommand : public Command } else { - report += string(name.begin(), name.end()) + ": " + string(val.begin(), val.end()) + "\n"; + report += " \"" + string(name.begin(), name.end()) + "\": \"" + + string(val.begin(), val.end()) + "\",\n"; } } - DialogBox(CrashHandlerInst, MAKEINTRESOURCE(IDD_CRASH_HANDLER), CrashHandlerWnd, - (DLGPROC)CrashHandlerProc); + rdcstr reportPath; - report += "\n\nRepro steps/Notes:\n\n" + reproSteps; + RENDERDOC_CreateBugReport(conv(wlogpath).c_str(), conv(wdump).c_str(), reportPath); + + for(size_t i = 0; i < reportPath.size(); i++) + if(reportPath[i] == '\\') + reportPath[i] = '/'; + + report += " \n\"report\": \"" + std::string(reportPath) + "\"\n"; + report += "}\n"; - if(uploadReport) { - mz_zip_archive zip; - ZeroMemory(&zip, sizeof(zip)); + wstring destjson = dumpFolder + L"\\report.json"; - wstring destzip = dumpFolder + L"\\report.zip"; + FILE *f = NULL; + _wfopen_s(&f, destjson.c_str(), L"w"); + fputs(report.c_str(), f); + fclose(f); - DeleteFileW(destzip.c_str()); + wchar_t *paramsAlloc = new wchar_t[512]; - mz_zip_writer_init_wfile(&zip, destzip.c_str(), 0); - mz_zip_writer_add_mem(&zip, "report.txt", report.c_str(), report.length(), - MZ_BEST_COMPRESSION); + ZeroMemory(paramsAlloc, sizeof(wchar_t) * 512); - if(uploadDump && !dump.empty()) - mz_zip_writer_add_wfile(&zip, "minidump.dmp", dump.c_str(), NULL, 0, MZ_BEST_COMPRESSION); + GetModuleFileNameW(NULL, paramsAlloc, 511); - if(uploadLog && !logpath.empty()) - mz_zip_writer_add_wfile(&zip, "error.log", logpath.c_str(), NULL, 0, MZ_BEST_COMPRESSION); + wchar_t *lastSlash = wcsrchr(paramsAlloc, '\\'); - mz_zip_writer_finalize_archive(&zip); - mz_zip_writer_end(&zip); + if(lastSlash) + *lastSlash = 0; - int timeout = 10000; - wstring body = L""; - int code = 0; + std::wstring exepath = paramsAlloc; - std::map params; + ZeroMemory(paramsAlloc, sizeof(wchar_t) * 512); - google_breakpad::HTTPUpload::SendRequest(L"https://renderdoc.org/bugsubmit", params, - dumpFolder + L"\\report.zip", L"report", &timeout, - &body, &code); + _snwprintf_s(paramsAlloc, 511, 511, L"%s/qrenderdoc.exe --crash %s", exepath.c_str(), + destjson.c_str()); - DeleteFileW(destzip.c_str()); + PROCESS_INFORMATION pi; + STARTUPINFOW si; + ZeroMemory(&pi, sizeof(pi)); + ZeroMemory(&si, sizeof(si)); + + BOOL success = + CreateProcessW(NULL, paramsAlloc, NULL, NULL, FALSE, 0, NULL, exepath.c_str(), &si, &pi); + + if(success && pi.hProcess) + { + WaitForSingleObject(pi.hProcess, INFINITE); + } + + if(pi.hProcess) + CloseHandle(pi.hProcess); + if(pi.hThread) + CloseHandle(pi.hThread); + + std::wstring wreport = conv(std::string(report)); + + DeleteFileW(wreport.c_str()); } } - if(!dump.empty()) - DeleteFileW(dump.c_str()); + if(!wdump.empty()) + DeleteFileW(wdump.c_str()); - if(!logpath.empty()) - DeleteFileW(logpath.c_str()); + if(!wlogpath.empty()) + DeleteFileW(wlogpath.c_str()); return 0; } @@ -878,7 +754,7 @@ int WINAPI wWinMain(_In_ HINSTANCE hInst, _In_opt_ HINSTANCE hPrevInstance, _In_ // perform an upgrade of the UI add_command("upgrade", new UpgradeCommand(env)); -#if defined(RELEASE) +#if CRASH_HANDLER // special WIN32 option for launching the crash handler add_command("crashhandle", new CrashHandlerCommand(env)); #endif diff --git a/renderdoccmd/resource.h b/renderdoccmd/resource.h index 9c8e4c9f4..86d0ec1fb 100644 Binary files a/renderdoccmd/resource.h and b/renderdoccmd/resource.h differ