diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index bb46b26c9..297fd77bb 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -2026,6 +2026,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
connect(m_ui.actionISOBrowser, &QAction::triggered, this, &MainWindow::onToolsISOBrowserTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
+ connect(m_ui.actionControllerTest, &QAction::triggered, g_emu_thread, &EmuThread::startControllerTest);
connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled);
connect(m_ui.actionCaptureGPUFrame, &QAction::triggered, g_emu_thread, &EmuThread::captureGPUFrameDump);
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui
index 42b5997ed..ac8a0ebbb 100644
--- a/src/duckstation-qt/mainwindow.ui
+++ b/src/duckstation-qt/mainwindow.ui
@@ -227,6 +227,7 @@
+
@@ -979,6 +980,11 @@
Free Camera
+
+
+ Controller Test
+
+
diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp
index 367ae3e87..2282b9cce 100644
--- a/src/duckstation-qt/qthost.cpp
+++ b/src/duckstation-qt/qthost.cpp
@@ -1334,6 +1334,45 @@ void EmuThread::captureGPUFrameDump()
System::StartRecordingGPUDump();
}
+void EmuThread::startControllerTest()
+{
+ static constexpr const char* PADTEST_URL = "https://downloads.duckstation.org/runtime-resources/padtest.psexe";
+
+ if (!isCurrentThread())
+ {
+ QMetaObject::invokeMethod(this, "startControllerTest", Qt::QueuedConnection);
+ return;
+ }
+
+ if (System::IsValid())
+ return;
+
+ std::string path = Path::Combine(EmuFolders::UserResources, "padtest.psexe");
+ if (FileSystem::FileExists(path.c_str()))
+ {
+ bootSystem(std::make_shared(std::move(path)));
+ return;
+ }
+
+ QtHost::RunOnUIThread([path = std::move(path)]() mutable {
+ {
+ auto lock = g_main_window->pauseAndLockSystem();
+ if (QMessageBox::question(
+ lock.getDialogParent(), tr("Confirm Download"),
+ tr("Your DuckStation installation does not have the padtest application "
+ "available.\n\nThis file is approximately 206KB, do you want to download it now?")) != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ if (!QtHost::DownloadFile(lock.getDialogParent(), tr("File Download"), PADTEST_URL, path.c_str()))
+ return;
+ }
+
+ g_emu_thread->startControllerTest();
+ });
+}
+
void EmuThread::runOnEmuThread(std::function callback)
{
callback();
diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h
index b3917649c..9f1f04375 100644
--- a/src/duckstation-qt/qthost.h
+++ b/src/duckstation-qt/qthost.h
@@ -212,6 +212,7 @@ public Q_SLOTS:
void clearInputBindStateFromSource(InputBindingKey key);
void reloadTextureReplacements();
void captureGPUFrameDump();
+ void startControllerTest();
void setGPUThreadRunIdle(bool active);
private Q_SLOTS: