diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml
index 3c4690f28..34d0226a5 100644
--- a/.github/workflows/linux-build.yml
+++ b/.github/workflows/linux-build.yml
@@ -27,7 +27,7 @@ jobs:
run: |
mkdir build-debug
cd build-debug
- cmake -DCMAKE_BUILD_TYPE=Debug ..
+ cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_QT_FRONTEND=OFF ..
make
- name: Compile release build
@@ -35,6 +35,6 @@ jobs:
run: |
mkdir build-release
cd build-release
- cmake -DCMAKE_BUILD_TYPE=Release ..
+ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_QT_FRONTEND=OFF ..
make
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0b87e6975..3cc7b09ec 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,6 +4,11 @@ project(duckstation C CXX)
# Pull in modules.
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules/")
+if(NOT ANDROID)
+ option(BUILD_SDL_FRONTEND "Build the SDL frontend" ON)
+ option(BUILD_QT_FRONTEND "Build the Qt frontend" ON)
+endif()
+
# Common include/library directories on Windows.
if(WIN32)
@@ -30,8 +35,15 @@ endif()
# Required libraries.
if(NOT ANDROID)
- find_package(SDL2 REQUIRED)
-else()
+ if(BUILD_SDL_FRONTEND)
+ find_package(SDL2 REQUIRED)
+ endif()
+ if(BUILD_QT_FRONTEND)
+ find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)
+ endif()
+endif()
+
+if(ANDROID)
find_package(EGL REQUIRED)
endif()
@@ -117,4 +129,4 @@ add_subdirectory(src)
if(ANDROID)
add_subdirectory(android/app/src/cpp)
-endif()
\ No newline at end of file
+endif()
diff --git a/dep/msvc/vsprops/QtCompile.props b/dep/msvc/vsprops/QtCompile.props
new file mode 100644
index 000000000..c2744d381
--- /dev/null
+++ b/dep/msvc/vsprops/QtCompile.props
@@ -0,0 +1,116 @@
+
+
+
+ $(SolutionDir)dep\msvc\qt5-x64\
+ $(QTDIRDefault)
+ $(QTDIR)\
+ false
+ true
+ $(QTDIR)include\
+ $(QTDIR)lib\
+ $(QTDIR)bin\
+ $(QTDIR)plugins\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(QtToolOutDir)moc_
+ d
+ $(QtDebugSuffix)
+ QtPlugins
+
+
+
+ QT_NO_DEBUG;%(PreprocessorDefinitions)
+ $(ProjectDir);%(AdditionalIncludeDirectories)
+ $(QtToolOutDir);%(AdditionalIncludeDirectories)
+ $(QtIncludeDir);%(AdditionalIncludeDirectories)
+
+
+ $(QtLibDir);%(AdditionalLibraryDirectories)
+ qtmain$(QtLibSuffix).lib;Qt5Core$(QtLibSuffix).lib;Qt5Gui$(QtLibSuffix).lib;Qt5Widgets$(QtLibSuffix).lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -DQT_NO_DEBUG -DNDEBUG $(MocDefines)
+
+ "-I$(QtIncludeDir)" "-I$(SolutionDir)src" -I.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QtResource
+
+
+ QtUi
+
+
+ QtMoc
+
+
+
diff --git a/dep/msvc/vsprops/QtCompile.targets b/dep/msvc/vsprops/QtCompile.targets
new file mode 100644
index 000000000..9ab1522a8
--- /dev/null
+++ b/dep/msvc/vsprops/QtCompile.targets
@@ -0,0 +1,10 @@
+
+
+
+ QtResourceClean;QtUiClean;QtMocClean;$(CleanDependsOn)
+
+
+
\ No newline at end of file
diff --git a/dep/msvc/vsprops/QtCompile.xml b/dep/msvc/vsprops/QtCompile.xml
new file mode 100644
index 000000000..976c2d2b8
--- /dev/null
+++ b/dep/msvc/vsprops/QtCompile.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/duckstation.sln b/duckstation.sln
index 04c9cd0c2..887207f59 100644
--- a/duckstation.sln
+++ b/duckstation.sln
@@ -27,6 +27,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "simpleini", "dep\simpleini\
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tinyxml2", "dep\tinyxml2\tinyxml2.vcxproj", "{933118A9-68C5-47B4-B151-B03C93961623}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-qt", "src\duckstation-qt\duckstation-qt.vcxproj", "{28F14272-0EC4-41BB-849F-182ADB81AF70}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -215,6 +217,18 @@ Global
{933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x64.Build.0 = Release|x64
{933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x86.ActiveCfg = Release|Win32
{933118A9-68C5-47B4-B151-B03C93961623}.ReleaseLTCG|x86.Build.0 = Release|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x64.ActiveCfg = Debug|x64
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x86.ActiveCfg = Debug|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Debug|x86.Build.0 = Debug|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x64.ActiveCfg = DebugFast|x64
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x86.ActiveCfg = DebugFast|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.DebugFast|x86.Build.0 = DebugFast|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x64.ActiveCfg = Release|x64
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x86.ActiveCfg = Release|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.Release|x86.Build.0 = Release|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4c372b8d3..66cea8d2e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,6 +1,11 @@
add_subdirectory(common)
add_subdirectory(core)
-if(NOT ANDROID)
+if(BUILD_SDL_FRONTEND)
add_subdirectory(duckstation)
endif()
+
+if(BUILD_QT_FRONTEND)
+ add_subdirectory(duckstation-qt)
+endif()
+
diff --git a/src/common/cd_image_cue.cpp b/src/common/cd_image_cue.cpp
index 464ee21f6..1c536a412 100644
--- a/src/common/cd_image_cue.cpp
+++ b/src/common/cd_image_cue.cpp
@@ -35,7 +35,7 @@ static std::string GetPathDirectory(const char* path)
const char* backslash_ptr = std::strrchr(path, '\\');
const char* slash_ptr;
if (forwardslash_ptr && backslash_ptr)
- slash_ptr = std::min(forwardslash_ptr, backslash_ptr);
+ slash_ptr = std::max(forwardslash_ptr, backslash_ptr);
else if (backslash_ptr)
slash_ptr = backslash_ptr;
else if (forwardslash_ptr)
diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp
index 17c5ea016..9d2fb1e1e 100644
--- a/src/core/game_list.cpp
+++ b/src/core/game_list.cpp
@@ -4,6 +4,7 @@
#include "bios.h"
#include "common/cd_image.h"
#include "common/iso_reader.h"
+#include "settings.h"
#include
#include
#include
@@ -194,11 +195,6 @@ std::optional GameList::GetRegionForPath(const char* image_path)
return GetRegionForImage(cdi.get());
}
-void GameList::AddDirectory(const char* path, bool recursive)
-{
- ScanDirectory(path, recursive);
-}
-
bool GameList::IsExeFileName(const char* path)
{
const char* extension = std::strrchr(path, '.');
@@ -404,6 +400,40 @@ private:
GameList::DatabaseMap& m_database;
};
+void GameList::AddDirectory(std::string path, bool recursive)
+{
+ auto iter = std::find_if(m_search_directories.begin(), m_search_directories.end(),
+ [&path](const DirectoryEntry& de) { return de.path == path; });
+ if (iter != m_search_directories.end())
+ {
+ iter->recursive = recursive;
+ return;
+ }
+
+ m_search_directories.push_back({path, recursive});
+}
+
+void GameList::SetDirectoriesFromSettings(SettingsInterface& si)
+{
+ m_search_directories.clear();
+
+ std::vector dirs = si.GetStringList("GameList", "Paths");
+ for (std::string& dir : dirs)
+ m_search_directories.push_back({std::move(dir), false});
+
+ dirs = si.GetStringList("GameList", "RecursivePaths");
+ for (std::string& dir : dirs)
+ m_search_directories.push_back({std::move(dir), true});
+}
+
+void GameList::RescanAllDirectories()
+{
+ m_entries.clear();
+
+ for (const DirectoryEntry& de : m_search_directories)
+ ScanDirectory(de.path.c_str(), de.recursive);
+}
+
bool GameList::ParseRedumpDatabase(const char* redump_dat_path)
{
tinyxml2::XMLDocument doc;
@@ -427,3 +457,8 @@ bool GameList::ParseRedumpDatabase(const char* redump_dat_path)
Log_InfoPrintf("Loaded %zu entries from Redump.org database '%s'", m_database.size(), redump_dat_path);
return true;
}
+
+void GameList::ClearDatabase()
+{
+ m_database.clear();
+}
diff --git a/src/core/game_list.h b/src/core/game_list.h
index 6f797cb8b..6a2b3eede 100644
--- a/src/core/game_list.h
+++ b/src/core/game_list.h
@@ -8,6 +8,8 @@
class CDImage;
+class SettingsInterface;
+
class GameList
{
public:
@@ -54,18 +56,28 @@ public:
const EntryList& GetEntries() const { return m_entries; }
const u32 GetEntryCount() const { return static_cast(m_entries.size()); }
- void AddDirectory(const char* path, bool recursive);
+ void AddDirectory(std::string path, bool recursive);
+ void SetDirectoriesFromSettings(SettingsInterface& si);
+ void RescanAllDirectories();
bool ParseRedumpDatabase(const char* redump_dat_path);
+ void ClearDatabase();
private:
+ struct DirectoryEntry
+ {
+ std::string path;
+ bool recursive;
+ };
+
static bool IsExeFileName(const char* path);
static bool GetExeListEntry(const char* path, GameListEntry* entry);
bool GetGameListEntry(const char* path, GameListEntry* entry);
-
void ScanDirectory(const char* path, bool recursive);
DatabaseMap m_database;
EntryList m_entries;
+
+ std::vector m_search_directories;
};
diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
new file mode 100644
index 000000000..eec9bf139
--- /dev/null
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -0,0 +1,32 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_executable(duckstation-qt
+ resources/icons.qrc
+ consolesettingswidget.cpp
+ consolesettingswidget.h
+ consolesettingswidget.ui
+ gamelistsettingswidget.cpp
+ gamelistsettingswidget.h
+ gamelistsettingswidget.ui
+ gamelistwidget.cpp
+ gamelistwidget.h
+ main.cpp
+ mainwindow.cpp
+ mainwindow.h
+ mainwindow.ui
+ opengldisplaywindow.cpp
+ opengldisplaywindow.h
+ qthostinterface.cpp
+ qthostinterface.h
+ qtsettingsinterface.cpp
+ qtsettingsinterface.h
+ qtutils.cpp
+ qtutils.h
+ settingsdialog.cpp
+ settingsdialog.h
+ settingsdialog.ui
+)
+
+target_link_libraries(duckstation-qt PRIVATE YBaseLib core common imgui glad Qt5::Core Qt5::Gui Qt5::Widgets)
diff --git a/src/duckstation-qt/consolesettingswidget.cpp b/src/duckstation-qt/consolesettingswidget.cpp
new file mode 100644
index 000000000..93462083a
--- /dev/null
+++ b/src/duckstation-qt/consolesettingswidget.cpp
@@ -0,0 +1,8 @@
+#include "consolesettingswidget.h"
+
+ConsoleSettingsWidget::ConsoleSettingsWidget(QWidget* parent /*= nullptr*/) : QWidget(parent)
+{
+ m_ui.setupUi(this);
+}
+
+ConsoleSettingsWidget::~ConsoleSettingsWidget() = default;
diff --git a/src/duckstation-qt/consolesettingswidget.h b/src/duckstation-qt/consolesettingswidget.h
new file mode 100644
index 000000000..2ec8bb05b
--- /dev/null
+++ b/src/duckstation-qt/consolesettingswidget.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+
+#include "ui_consolesettingswidget.h"
+
+class ConsoleSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ConsoleSettingsWidget(QWidget* parent = nullptr);
+ ~ConsoleSettingsWidget();
+
+private:
+ Ui::ConsoleSettingsWidget m_ui;
+};
diff --git a/src/duckstation-qt/consolesettingswidget.ui b/src/duckstation-qt/consolesettingswidget.ui
new file mode 100644
index 000000000..cfe94d238
--- /dev/null
+++ b/src/duckstation-qt/consolesettingswidget.ui
@@ -0,0 +1,114 @@
+
+
+ ConsoleSettingsWidget
+
+
+
+ 0
+ 0
+ 502
+ 308
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ Region:
+
+
+
+ -
+
+
-
+
+ Auto-Detect
+
+
+ -
+
+ NTSC-U (US)
+
+
+ -
+
+ NTSC-J (Japan)
+
+
+ -
+
+ PAL (Europe, Australia)
+
+
+
+
+ -
+
+
+ BIOS Path:
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ ...
+
+
+
+
+
+ -
+
+
+ Enable TTY Output
+
+
+
+ -
+
+
+ Fast Boot
+
+
+
+ -
+
+
+ Enable Speed Limiter
+
+
+
+ -
+
+
+ Pause On Start
+
+
+
+
+
+
+
+
+
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
new file mode 100644
index 000000000..3f88d915b
--- /dev/null
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -0,0 +1,426 @@
+
+
+
+
+ DebugFast
+ Win32
+
+
+ DebugFast
+ x64
+
+
+ Debug
+ Win32
+
+
+ Debug
+ x64
+
+
+ ReleaseLTCG
+ Win32
+
+
+ ReleaseLTCG
+ x64
+
+
+ Release
+ Win32
+
+
+ Release
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {43540154-9e1e-409c-834f-b84be5621388}
+
+
+ {bb08260f-6fbc-46af-8924-090ee71360c6}
+
+
+ {b56ce698-7300-4fa5-9609-942f1d05c5a2}
+
+
+ {ee054e08-3799-4a59-a422-18259c105ffd}
+
+
+ {868b98c8-65a1-494b-8346-250a73a48c0a}
+
+
+
+
+ Document
+
+
+ Document
+
+
+ Document
+
+
+ Document
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {28F14272-0EC4-41BB-849F-182ADB81AF70}
+ Win32Proj
+ duckstation-qt
+ 10.0
+
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ true
+ $(SolutionDir)bin\$(Platform)\
+
+
+ true
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ true
+ $(SolutionDir)bin\$(Platform)\
+
+
+ false
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ false
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ false
+ $(SolutionDir)bin\$(Platform)\
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ false
+ $(SolutionDir)bin\$(Platform)\
+
+
+
+
+
+ Level4
+ Disabled
+ _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+
+
+ Console
+ true
+ $(SolutionDir)dep\msvc\lib32-debug;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories)
+ Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+
+
+ Console
+ true
+ $(SolutionDir)dep\msvc\lib64-debug;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories)
+ Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories)
+ Default
+ true
+ false
+ stdcpp17
+ false
+
+
+ Console
+ true
+ $(SolutionDir)dep\msvc\lib32-debug;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories)
+ Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories)
+ Default
+ true
+ false
+ stdcpp17
+ false
+
+
+ Console
+ true
+ $(SolutionDir)dep\msvc\lib64-debug;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories)
+ Qt5Cored.lib;Qt5Guid.lib;Qt5Widgetsd.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+
+
+ Console
+ true
+ true
+ true
+ $(SolutionDir)dep\msvc\lib32;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories)
+ Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x86\include;%(AdditionalIncludeDirectories)
+ true
+ true
+ stdcpp17
+ true
+
+
+ Console
+ true
+ true
+ true
+ $(SolutionDir)dep\msvc\lib32;$(SolutionDir)dep\msvc\qt5-x86\lib;%(AdditionalLibraryDirectories)
+ Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies)
+ UseLinkTimeCodeGeneration
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+
+
+ Console
+ true
+ true
+ true
+ $(SolutionDir)dep\msvc\lib64;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories)
+ Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\YBaseLib\Include;$(SolutionDir)dep\glad\Include;$(SolutionDir)src;$(SolutionDir)dep\msvc\qt5-x64\include;%(AdditionalIncludeDirectories)
+ true
+ true
+ stdcpp17
+ true
+
+
+ Console
+ true
+ true
+ true
+ $(SolutionDir)dep\msvc\lib64;$(SolutionDir)dep\msvc\qt5-x64\lib;%(AdditionalLibraryDirectories)
+ Qt5Core.lib;Qt5Gui.lib;Qt5Widgets.lib;%(AdditionalDependencies)
+ UseLinkTimeCodeGeneration
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
new file mode 100644
index 000000000..c10cb7a9f
--- /dev/null
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ resources
+
+
+
+
+ {3b2587ae-ce3b-4eb5-ada2-237e853620cf}
+
+
+
\ No newline at end of file
diff --git a/src/duckstation-qt/gamelistsettingswidget.cpp b/src/duckstation-qt/gamelistsettingswidget.cpp
new file mode 100644
index 000000000..ec47b9028
--- /dev/null
+++ b/src/duckstation-qt/gamelistsettingswidget.cpp
@@ -0,0 +1,230 @@
+#include "gamelistsettingswidget.h"
+#include "qthostinterface.h"
+#include "qtutils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+class GameListSearchDirectoriesModel : public QAbstractTableModel
+{
+public:
+ GameListSearchDirectoriesModel(QSettings& settings) : m_settings(settings) {}
+
+ ~GameListSearchDirectoriesModel() = default;
+
+ int columnCount(const QModelIndex& parent) const override
+ {
+ if (parent.isValid())
+ return 0;
+
+ return 2;
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override
+ {
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
+ return {};
+
+ if (section == 0)
+ return tr("Path");
+ else
+ return tr("Recursive");
+ }
+
+ int rowCount(const QModelIndex& parent) const override
+ {
+ if (parent.isValid())
+ return 0;
+
+ return static_cast(m_entries.size());
+ }
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ return {};
+
+ const int row = index.row();
+ const int column = index.column();
+ if (row < 0 || row >= static_cast(m_entries.size()))
+ return {};
+
+ const Entry& entry = m_entries[row];
+ if (role == Qt::CheckStateRole)
+ {
+ if (column == 1)
+ return entry.recursive ? Qt::Checked : Qt::Unchecked;
+ }
+ else if (role == Qt::DisplayRole)
+ {
+ if (column == 0)
+ return entry.path;
+ }
+
+ return {};
+ }
+
+ void addEntry(const QString& path, bool recursive)
+ {
+ if (std::find_if(m_entries.begin(), m_entries.end(), [path](const Entry& e) { return e.path == path; }) !=
+ m_entries.end())
+ {
+ return;
+ }
+
+ beginInsertRows(QModelIndex(), static_cast(m_entries.size()), static_cast(m_entries.size() + 1));
+ m_entries.push_back({path, recursive});
+ endInsertRows();
+ }
+
+ void removeEntry(int row)
+ {
+ if (row < 0 || row >= static_cast(m_entries.size()))
+ return;
+
+ beginRemoveRows(QModelIndex(), row, row);
+ m_entries.erase(m_entries.begin() + row);
+ endRemoveRows();
+ }
+
+ void loadFromSettings()
+ {
+ QStringList path_list = m_settings.value(QStringLiteral("GameList/Paths")).toStringList();
+ for (QString& entry : path_list)
+ m_entries.push_back({std::move(entry), false});
+
+ path_list = m_settings.value(QStringLiteral("GameList/RecursivePaths")).toStringList();
+ for (QString& entry : path_list)
+ m_entries.push_back({std::move(entry), true});
+ }
+
+ void saveToSettings()
+ {
+ QStringList paths;
+ QStringList recursive_paths;
+
+ for (const Entry& entry : m_entries)
+ {
+ if (entry.recursive)
+ recursive_paths.push_back(entry.path);
+ else
+ paths.push_back(entry.path);
+ }
+
+ if (paths.empty())
+ m_settings.remove(QStringLiteral("GameList/Paths"));
+ else
+ m_settings.setValue(QStringLiteral("GameList/Paths"), paths);
+
+ if (recursive_paths.empty())
+ m_settings.remove(QStringLiteral("GameList/RecursivePaths"));
+ else
+ m_settings.setValue(QStringLiteral("GameList/RecursivePaths"), recursive_paths);
+ }
+
+private:
+ struct Entry
+ {
+ QString path;
+ bool recursive;
+ };
+
+ QSettings& m_settings;
+ std::vector m_entries;
+};
+
+GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
+ : QWidget(parent), m_host_interface(host_interface)
+{
+ m_ui.setupUi(this);
+
+ QSettings& qsettings = host_interface->getQSettings();
+
+ m_search_directories_model = new GameListSearchDirectoriesModel(qsettings);
+ m_search_directories_model->loadFromSettings();
+ m_ui.redumpDatabasePath->setText(qsettings.value("GameList/RedumpDatabasePath").toString());
+ m_ui.searchDirectoryList->setModel(m_search_directories_model);
+ m_ui.searchDirectoryList->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_ui.searchDirectoryList->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_ui.searchDirectoryList->setAlternatingRowColors(true);
+ m_ui.searchDirectoryList->setShowGrid(false);
+ m_ui.searchDirectoryList->verticalHeader()->hide();
+ m_ui.searchDirectoryList->setCurrentIndex({});
+
+ connect(m_ui.addSearchDirectoryButton, &QToolButton::pressed, this,
+ &GameListSettingsWidget::onAddSearchDirectoryButtonPressed);
+ connect(m_ui.removeSearchDirectoryButton, &QToolButton::pressed, this,
+ &GameListSettingsWidget::onRemoveSearchDirectoryButtonPressed);
+ connect(m_ui.refreshGameListButton, &QToolButton::pressed, this,
+ &GameListSettingsWidget::onRefreshGameListButtonPressed);
+ connect(m_ui.browseRedumpPath, &QToolButton::pressed, this, &GameListSettingsWidget::onBrowseRedumpPathButtonPressed);
+ connect(m_ui.downloadRedumpDatabase, &QToolButton::pressed, this,
+ &GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed);
+}
+
+GameListSettingsWidget::~GameListSettingsWidget() = default;
+
+void GameListSettingsWidget::resizeEvent(QResizeEvent* event)
+{
+ QWidget::resizeEvent(event);
+
+ QtUtils::ResizeColumnsForTableView(m_ui.searchDirectoryList, {-1, 100});
+}
+
+void GameListSettingsWidget::onAddSearchDirectoryButtonPressed()
+{
+ QString dir = QFileDialog::getExistingDirectory(this, tr("Select Search Directory"));
+ if (dir.isEmpty())
+ return;
+
+ QMessageBox::StandardButton selection =
+ QMessageBox::question(this, tr("Scan Recursively?"),
+ tr("Would you like to scan the directory \"%1\" recursively?\n\nScanning recursively takes "
+ "more time, but will identify files in subdirectories.")
+ .arg(dir),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
+ if (selection == QMessageBox::Cancel)
+ return;
+
+ const bool recursive = (selection == QMessageBox::Yes);
+ m_search_directories_model->addEntry(dir, recursive);
+ m_search_directories_model->saveToSettings();
+ m_host_interface->refreshGameList(false);
+}
+
+void GameListSettingsWidget::onRemoveSearchDirectoryButtonPressed()
+{
+ QModelIndexList selection = m_ui.searchDirectoryList->selectionModel()->selectedIndexes();
+ if (selection.size() < 1)
+ return;
+
+ const int row = selection[0].row();
+ m_search_directories_model->removeEntry(row);
+ m_search_directories_model->saveToSettings();
+ m_host_interface->refreshGameList(false);
+}
+
+void GameListSettingsWidget::onRefreshGameListButtonPressed()
+{
+ m_host_interface->refreshGameList(true);
+}
+
+void GameListSettingsWidget::onBrowseRedumpPathButtonPressed()
+{
+ QString filename = QFileDialog::getOpenFileName(this, tr("Select Redump Database File"), QString(),
+ tr("Redump Database Files (*.dat)"));
+ if (filename.isEmpty())
+ return;
+
+ m_ui.redumpDatabasePath->setText(filename);
+ m_host_interface->getQSettings().setValue("GameList/RedumpDatabasePath", filename);
+ m_host_interface->updateGameListDatabase(true);
+}
+
+void GameListSettingsWidget::onDownloadRedumpDatabaseButtonPressed()
+{
+ QMessageBox::information(this, tr("TODO"), tr("TODO"));
+}
diff --git a/src/duckstation-qt/gamelistsettingswidget.h b/src/duckstation-qt/gamelistsettingswidget.h
new file mode 100644
index 000000000..ab7a20f2d
--- /dev/null
+++ b/src/duckstation-qt/gamelistsettingswidget.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+
+#include "ui_gamelistsettingswidget.h"
+
+class QtHostInterface;
+
+class GameListSearchDirectoriesModel;
+
+class GameListSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent = nullptr);
+ ~GameListSettingsWidget();
+
+private Q_SLOTS:
+ void onAddSearchDirectoryButtonPressed();
+ void onRemoveSearchDirectoryButtonPressed();
+ void onRefreshGameListButtonPressed();
+ void onBrowseRedumpPathButtonPressed();
+ void onDownloadRedumpDatabaseButtonPressed();
+
+protected:
+ void resizeEvent(QResizeEvent* event);
+
+private:
+ QtHostInterface* m_host_interface;
+
+ Ui::GameListSettingsWidget m_ui;
+
+ GameListSearchDirectoriesModel* m_search_directories_model = nullptr;
+};
diff --git a/src/duckstation-qt/gamelistsettingswidget.ui b/src/duckstation-qt/gamelistsettingswidget.ui
new file mode 100644
index 000000000..fe52a8969
--- /dev/null
+++ b/src/duckstation-qt/gamelistsettingswidget.ui
@@ -0,0 +1,150 @@
+
+
+ GameListSettingsWidget
+
+
+
+ 0
+ 0
+ 527
+ 376
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
-
+
+
+ Search Directories
+
+
+
+ -
+
+
+ -
+
+
-
+
+
+ Add
+
+
+
+ :/icons/list-add.png:/icons/list-add.png
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+
+ -
+
+
+ Remove
+
+
+
+ :/icons/list-remove.png:/icons/list-remove.png
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Refresh
+
+
+
+ :/icons/view-refresh.png:/icons/view-refresh.png
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+
+
+
+ -
+
+
+ Redump Database Path
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ Browse...
+
+
+
+ :/icons/system-search.png:/icons/system-search.png
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+ false
+
+
+
+ -
+
+
+ Download...
+
+
+
+ :/icons/applications-internet.png:/icons/applications-internet.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp
new file mode 100644
index 000000000..9d7c0abfc
--- /dev/null
+++ b/src/duckstation-qt/gamelistwidget.cpp
@@ -0,0 +1,164 @@
+#include "gamelistwidget.h"
+#include "core/settings.h"
+#include "qthostinterface.h"
+#include "qtutils.h"
+#include
+
+class GameListModel : public QAbstractTableModel
+{
+public:
+ enum Column : int
+ {
+ // Column_Icon,
+ Column_Code,
+ Column_Title,
+ Column_Region,
+ Column_Size,
+
+ Column_Count
+ };
+
+ GameListModel(GameList* game_list, QObject* parent = nullptr)
+ : QAbstractTableModel(parent), m_game_list(game_list), m_size(static_cast(m_game_list->GetEntryCount()))
+ {
+ }
+ ~GameListModel() = default;
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ if (parent.isValid())
+ return 0;
+
+ return static_cast(m_game_list->GetEntryCount());
+ }
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ if (parent.isValid())
+ return 0;
+
+ return Column_Count;
+ }
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ return {};
+
+ if (role != Qt::DisplayRole)
+ return {};
+
+ const int row = index.row();
+ if (row < 0 || row >= static_cast(m_game_list->GetEntryCount()))
+ return {};
+
+ const GameList::GameListEntry& ge = m_game_list->GetEntries()[row];
+ switch (index.column())
+ {
+ case Column_Code:
+ return QString::fromStdString(ge.code);
+
+ case Column_Title:
+ return QString::fromStdString(ge.title);
+
+ case Column_Region:
+ return QString(Settings::GetConsoleRegionName(ge.region));
+
+ case Column_Size:
+ return QString("%1 MB").arg(static_cast(ge.total_size) / 1048576.0, 0, 'f', 2);
+
+ default:
+ return {};
+ }
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override
+ {
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
+ return {};
+
+ switch (section)
+ {
+ case Column_Code:
+ return "Code";
+
+ case Column_Title:
+ return "Title";
+
+ case Column_Region:
+ return "Region";
+
+ case Column_Size:
+ return "Size";
+
+ default:
+ return {};
+ }
+ }
+
+ void refresh()
+ {
+ if (m_size > 0)
+ {
+ beginRemoveRows(QModelIndex(), 0, m_size - 1);
+ endRemoveRows();
+ }
+
+ m_size = static_cast(m_game_list->GetEntryCount());
+ beginInsertRows(QModelIndex(), 0, m_size);
+ endInsertRows();
+ }
+
+private:
+ GameList* m_game_list;
+ int m_size;
+};
+
+GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QStackedWidget(parent) {}
+
+GameListWidget::~GameListWidget() = default;
+
+void GameListWidget::initialize(QtHostInterface* host_interface)
+{
+ m_host_interface = host_interface;
+ m_game_list = host_interface->getGameList();
+
+ connect(m_host_interface, &QtHostInterface::gameListRefreshed, this, &GameListWidget::onGameListRefreshed);
+
+ m_table_model = new GameListModel(m_game_list, this);
+ m_table_view = new QTableView(this);
+ m_table_view->setModel(m_table_model);
+ m_table_view->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table_view->setAlternatingRowColors(true);
+ m_table_view->setShowGrid(false);
+ m_table_view->setCurrentIndex({});
+ m_table_view->verticalHeader()->hide();
+ m_table_view->resizeColumnsToContents();
+
+ connect(m_table_view, &QTableView::doubleClicked, this, &GameListWidget::onTableViewItemDoubleClicked);
+
+ insertWidget(0, m_table_view);
+ setCurrentIndex(0);
+}
+
+void GameListWidget::onGameListRefreshed()
+{
+ m_table_model->refresh();
+}
+
+void GameListWidget::onTableViewItemDoubleClicked(const QModelIndex& index)
+{
+ if (!index.isValid() || index.row() >= static_cast(m_game_list->GetEntryCount()))
+ return;
+
+ const GameList::GameListEntry& entry = m_game_list->GetEntries().at(index.row());
+ emit bootEntryRequested(entry);
+}
+
+void GameListWidget::resizeEvent(QResizeEvent* event)
+{
+ QStackedWidget::resizeEvent(event);
+
+ QtUtils::ResizeColumnsForTableView(m_table_view, {100, -1, 100, 100});
+}
diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h
new file mode 100644
index 000000000..864643a44
--- /dev/null
+++ b/src/duckstation-qt/gamelistwidget.h
@@ -0,0 +1,36 @@
+#pragma once
+#include "core/game_list.h"
+#include
+#include
+
+class GameListModel;
+
+class QtHostInterface;
+
+class GameListWidget : public QStackedWidget
+{
+ Q_OBJECT
+
+public:
+ GameListWidget(QWidget* parent = nullptr);
+ ~GameListWidget();
+
+ void initialize(QtHostInterface* host_interface);
+
+Q_SIGNALS:
+ void bootEntryRequested(const GameList::GameListEntry& entry);
+
+private Q_SLOTS:
+ void onGameListRefreshed();
+ void onTableViewItemDoubleClicked(const QModelIndex& index);
+
+protected:
+ void resizeEvent(QResizeEvent* event);
+
+private:
+ QtHostInterface* m_host_interface = nullptr;
+ GameList* m_game_list = nullptr;
+
+ GameListModel* m_table_model = nullptr;
+ QTableView* m_table_view = nullptr;
+};
diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp
new file mode 100644
index 000000000..de0189f1b
--- /dev/null
+++ b/src/duckstation-qt/main.cpp
@@ -0,0 +1,28 @@
+#include "YBaseLib/Log.h"
+#include "mainwindow.h"
+#include "qthostinterface.h"
+#include
+#include
+
+static void InitLogging()
+{
+ // set log flags
+ // g_pLog->SetConsoleOutputParams(true);
+ g_pLog->SetConsoleOutputParams(true, nullptr, LOGLEVEL_PROFILE);
+ g_pLog->SetFilterLevel(LOGLEVEL_PROFILE);
+ // g_pLog->SetDebugOutputParams(true);
+}
+
+int main(int argc, char* argv[])
+{
+ InitLogging();
+
+ QApplication app(argc, argv);
+
+ std::unique_ptr host_interface = std::make_unique();
+
+ std::unique_ptr window = std::make_unique(host_interface.get());
+ window->show();
+
+ return app.exec();
+}
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
new file mode 100644
index 000000000..b7d718f60
--- /dev/null
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -0,0 +1,207 @@
+#include "mainwindow.h"
+#include "core/game_list.h"
+#include "core/settings.h"
+#include "gamelistwidget.h"
+#include "qthostinterface.h"
+#include "qtsettingsinterface.h"
+#include "settingsdialog.h"
+#include
+
+static constexpr char DISC_IMAGE_FILTER[] =
+ "All File Types (*.bin *.img *.cue *.exe *.psexe);;Single-Track Raw Images (*.bin *.img);;Cue Sheets "
+ "(*.cue);;PlayStation Executables (*.exe *.psexe)";
+
+MainWindow::MainWindow(QtHostInterface* host_interface) : QMainWindow(nullptr), m_host_interface(host_interface)
+{
+ m_ui.setupUi(this);
+ setupAdditionalUi();
+ connectSignals();
+
+ // force a scan of the game list
+ m_host_interface->refreshGameList();
+
+ resize(750, 690);
+}
+
+MainWindow::~MainWindow()
+{
+ m_host_interface->destroyDisplayWidget();
+}
+
+void MainWindow::onEmulationStarting()
+{
+ switchToEmulationView();
+ updateEmulationActions(true, false);
+
+ // we need the surface visible..
+ QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
+}
+
+void MainWindow::onEmulationStarted()
+{
+ updateEmulationActions(false, true);
+ m_emulation_running = true;
+}
+
+void MainWindow::onEmulationStopped()
+{
+ updateEmulationActions(false, false);
+ switchToGameListView();
+ m_emulation_running = false;
+}
+
+void MainWindow::onEmulationPaused(bool paused)
+{
+ m_ui.actionPause->setChecked(paused);
+}
+
+void MainWindow::onStartDiscActionTriggered()
+{
+ QString filename =
+ QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr);
+ if (filename.isEmpty())
+ return;
+
+ m_host_interface->bootSystem(std::move(filename), QString());
+}
+
+void MainWindow::onChangeDiscActionTriggered()
+{
+ QMenu menu(tr("Change Disc..."), this);
+ QAction* from_file = menu.addAction(tr("From File..."));
+ QAction* from_game_list = menu.addAction(tr("From Game List"));
+
+ QAction* selected = menu.exec(QCursor::pos());
+ if (selected == from_file)
+ {
+ QString filename =
+ QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr);
+ if (filename.isEmpty())
+ return;
+
+ m_host_interface->changeDisc(filename);
+ }
+ else if (selected == from_game_list)
+ {
+ m_host_interface->pauseSystem(true);
+ switchToGameListView();
+ }
+}
+
+void MainWindow::onStartBiosActionTriggered()
+{
+ m_host_interface->bootSystem(QString(), QString());
+}
+
+void MainWindow::onOpenDirectoryActionTriggered() {}
+
+void MainWindow::onExitActionTriggered() {}
+
+void MainWindow::onFullscreenActionToggled(bool fullscreen) {}
+
+void MainWindow::onGitHubRepositoryActionTriggered() {}
+
+void MainWindow::onIssueTrackerActionTriggered() {}
+
+void MainWindow::onAboutActionTriggered() {}
+
+void MainWindow::setupAdditionalUi()
+{
+ m_game_list_widget = new GameListWidget(m_ui.mainContainer);
+ m_game_list_widget->initialize(m_host_interface);
+ m_ui.mainContainer->insertWidget(0, m_game_list_widget);
+
+ QWidget* display_widget = m_host_interface->createDisplayWidget(m_ui.mainContainer);
+ m_ui.mainContainer->insertWidget(1, display_widget);
+
+ m_ui.mainContainer->setCurrentIndex(0);
+}
+
+void MainWindow::updateEmulationActions(bool starting, bool running)
+{
+ m_ui.actionStartDisc->setDisabled(starting || running);
+ m_ui.actionStartBios->setDisabled(starting || running);
+ m_ui.actionOpenDirectory->setDisabled(starting || running);
+ m_ui.actionPowerOff->setDisabled(starting || running);
+
+ m_ui.actionPowerOff->setDisabled(starting || !running);
+ m_ui.actionReset->setDisabled(starting || !running);
+ m_ui.actionPause->setDisabled(starting || !running);
+ m_ui.actionChangeDisc->setDisabled(starting || !running);
+
+ m_ui.actionLoadState->setDisabled(starting);
+ m_ui.actionSaveState->setDisabled(starting);
+
+ m_ui.actionFullscreen->setDisabled(starting || !running);
+}
+
+void MainWindow::switchToGameListView()
+{
+ m_ui.mainContainer->setCurrentIndex(0);
+}
+
+void MainWindow::switchToEmulationView()
+{
+ m_ui.mainContainer->setCurrentIndex(1);
+}
+
+void MainWindow::connectSignals()
+{
+ updateEmulationActions(false, false);
+ onEmulationPaused(false);
+
+ connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered);
+ connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBiosActionTriggered);
+ connect(m_ui.actionChangeDisc, &QAction::triggered, this, &MainWindow::onChangeDiscActionTriggered);
+ connect(m_ui.actionOpenDirectory, &QAction::triggered, this, &MainWindow::onOpenDirectoryActionTriggered);
+ connect(m_ui.actionPowerOff, &QAction::triggered, m_host_interface, &QtHostInterface::powerOffSystem);
+ connect(m_ui.actionReset, &QAction::triggered, m_host_interface, &QtHostInterface::resetSystem);
+ connect(m_ui.actionPause, &QAction::toggled, m_host_interface, &QtHostInterface::pauseSystem);
+ connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::onExitActionTriggered);
+ connect(m_ui.actionFullscreen, &QAction::toggled, this, &MainWindow::onFullscreenActionToggled);
+ connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::Count); });
+ connect(m_ui.actionGameListSettings, &QAction::triggered,
+ [this]() { doSettings(SettingsDialog::Category::GameListSettings); });
+ connect(m_ui.actionCPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::CPUSettings); });
+ connect(m_ui.actionGPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::GPUSettings); });
+ connect(m_ui.actionAudioSettings, &QAction::triggered,
+ [this]() { doSettings(SettingsDialog::Category::AudioSettings); });
+ connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered);
+ connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered);
+ connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
+
+ connect(m_host_interface, &QtHostInterface::emulationStarting, this, &MainWindow::onEmulationStarting);
+ connect(m_host_interface, &QtHostInterface::emulationStarted, this, &MainWindow::onEmulationStarted);
+ connect(m_host_interface, &QtHostInterface::emulationStopped, this, &MainWindow::onEmulationStopped);
+ connect(m_host_interface, &QtHostInterface::emulationPaused, this, &MainWindow::onEmulationPaused);
+
+ connect(m_game_list_widget, &GameListWidget::bootEntryRequested, [this](const GameList::GameListEntry& entry) {
+ // if we're not running, boot the system, otherwise swap discs
+ QString path = QString::fromStdString(entry.path);
+ if (!m_emulation_running)
+ {
+ m_host_interface->bootSystem(path, QString());
+ }
+ else
+ {
+ m_host_interface->changeDisc(path);
+ m_host_interface->pauseSystem(false);
+ switchToEmulationView();
+ }
+ });
+}
+
+void MainWindow::doSettings(SettingsDialog::Category category)
+{
+ if (!m_settings_dialog)
+ m_settings_dialog = new SettingsDialog(m_host_interface, this);
+
+ if (!m_settings_dialog->isVisible())
+ {
+ m_settings_dialog->setModal(false);
+ m_settings_dialog->show();
+ }
+
+ if (category != SettingsDialog::Category::Count)
+ m_settings_dialog->setCategory(category);
+}
\ No newline at end of file
diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h
new file mode 100644
index 000000000..c0c7dd8d2
--- /dev/null
+++ b/src/duckstation-qt/mainwindow.h
@@ -0,0 +1,57 @@
+#pragma once
+#include
+#include
+#include
+#include
+
+#include "settingsdialog.h"
+#include "ui_mainwindow.h"
+
+class GameList;
+class GameListWidget;
+class QtHostInterface;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ explicit MainWindow(QtHostInterface* host_interface);
+ ~MainWindow();
+
+public Q_SLOTS:
+ void onEmulationStarting();
+ void onEmulationStarted();
+ void onEmulationStopped();
+ void onEmulationPaused(bool paused);
+
+ void onStartDiscActionTriggered();
+ void onChangeDiscActionTriggered();
+ void onStartBiosActionTriggered();
+ void onOpenDirectoryActionTriggered();
+ void onExitActionTriggered();
+ void onFullscreenActionToggled(bool fullscreen);
+ void onGitHubRepositoryActionTriggered();
+ void onIssueTrackerActionTriggered();
+ void onAboutActionTriggered();
+
+private:
+ void createGameList();
+ void setupAdditionalUi();
+ void connectSignals();
+ void updateEmulationActions(bool starting, bool running);
+ void switchToGameListView();
+ void switchToEmulationView();
+ void doSettings(SettingsDialog::Category category = SettingsDialog::Category::Count);
+
+ Ui::MainWindow m_ui;
+
+ QtHostInterface* m_host_interface = nullptr;
+
+ std::unique_ptr m_game_list;
+ GameListWidget* m_game_list_widget = nullptr;
+
+ SettingsDialog* m_settings_dialog = nullptr;
+
+ bool m_emulation_running = false;
+};
diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui
new file mode 100644
index 000000000..70c5b5438
--- /dev/null
+++ b/src/duckstation-qt/mainwindow.ui
@@ -0,0 +1,368 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 754
+ 600
+
+
+
+ DuckStation
+
+
+
+ :/icons/duck.png:/icons/duck.png
+
+
+
+ 0
+
+
+
+
+
+
+
+ toolBar
+
+
+
+ 32
+ 32
+
+
+
+ Qt::ToolButtonTextUnderIcon
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :/icons/drive-optical.png:/icons/drive-optical.png
+
+
+ Start &Disc...
+
+
+
+
+
+ :/icons/drive-removable-media.png:/icons/drive-removable-media.png
+
+
+ Start &BIOS
+
+
+
+
+
+ :/icons/system-shutdown.png:/icons/system-shutdown.png
+
+
+ Power &Off
+
+
+
+
+
+ :/icons/view-refresh.png:/icons/view-refresh.png
+
+
+ &Reset
+
+
+
+
+ true
+
+
+
+ :/icons/media-playback-pause.png:/icons/media-playback-pause.png
+
+
+ &Pause
+
+
+
+
+
+ :/icons/document-open.png:/icons/document-open.png
+
+
+ &Load State
+
+
+
+
+
+ :/icons/document-save.png:/icons/document-save.png
+
+
+ &Save State
+
+
+
+
+ E&xit
+
+
+
+
+
+ :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png
+
+
+ &Console Settings...
+
+
+
+
+
+ :/icons/input-gaming.png:/icons/input-gaming.png
+
+
+ &Port Settings...
+
+
+
+
+
+ :/icons/applications-other.png:/icons/applications-other.png
+
+
+ &CPU Settings...
+
+
+
+
+
+ :/icons/video-display.png:/icons/video-display.png
+
+
+ &GPU Settings...
+
+
+
+
+
+ :/icons/view-fullscreen.png:/icons/view-fullscreen.png
+
+
+ Fullscreen
+
+
+
+
+ true
+
+
+ Hardware (D3D11)
+
+
+
+
+ true
+
+
+ Hardware (OpenGL)
+
+
+
+
+ true
+
+
+ Software
+
+
+
+
+ true
+
+
+ Interpreter (Slowest)
+
+
+
+
+ true
+
+
+ Cached Interpreter (Slower)
+
+
+
+
+ true
+
+
+ Recompiler (Fastest)
+
+
+
+
+ Resolution Scale
+
+
+
+
+ &GitHub Repository...
+
+
+
+
+ &Issue Tracker...
+
+
+
+
+ &About...
+
+
+
+
+
+ :/icons/media-optical.png:/icons/media-optical.png
+
+
+ Change Disc...
+
+
+
+
+
+ :/icons/audio-card.png:/icons/audio-card.png
+
+
+ Audio Settings...
+
+
+
+
+
+ :/icons/folder-open.png:/icons/folder-open.png
+
+
+ Game List Settings...
+
+
+
+
+
+ :/icons/edit-find.png:/icons/edit-find.png
+
+
+ Open Directory...
+
+
+
+
+
+ :/icons/applications-system.png:/icons/applications-system.png
+
+
+ &Settings...
+
+
+
+
+
+
+
+
diff --git a/src/duckstation-qt/opengldisplaywindow.cpp b/src/duckstation-qt/opengldisplaywindow.cpp
new file mode 100644
index 000000000..ba1c72402
--- /dev/null
+++ b/src/duckstation-qt/opengldisplaywindow.cpp
@@ -0,0 +1,430 @@
+#include "opengldisplaywindow.h"
+#include "YBaseLib/Assert.h"
+#include "YBaseLib/Log.h"
+#include
+#include
+Log_SetChannel(OpenGLDisplayWindow);
+
+static thread_local QOpenGLContext* s_thread_gl_context;
+
+static void* GetProcAddressCallback(const char* name)
+{
+ QOpenGLContext* ctx = s_thread_gl_context;
+ if (!ctx)
+ return nullptr;
+
+ return (void*)ctx->getProcAddress(name);
+}
+
+class OpenGLHostDisplayTexture : public HostDisplayTexture
+{
+public:
+ OpenGLHostDisplayTexture(GLuint id, u32 width, u32 height) : m_id(id), m_width(width), m_height(height) {}
+ ~OpenGLHostDisplayTexture() override { glDeleteTextures(1, &m_id); }
+
+ void* GetHandle() const override { return reinterpret_cast(static_cast(m_id)); }
+ u32 GetWidth() const override { return m_width; }
+ u32 GetHeight() const override { return m_height; }
+
+ GLuint GetGLID() const { return m_id; }
+
+ static std::unique_ptr Create(u32 width, u32 height, const void* initial_data,
+ u32 initial_data_stride)
+ {
+ GLuint id;
+ glGenTextures(1, &id);
+
+ GLint old_texture_binding = 0;
+ glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding);
+
+ // TODO: Set pack width
+ Assert(!initial_data || initial_data_stride == (width * sizeof(u32)));
+
+ glBindTexture(GL_TEXTURE_2D, id);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, initial_data);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ glBindTexture(GL_TEXTURE_2D, id);
+ return std::make_unique(id, width, height);
+ }
+
+private:
+ GLuint m_id;
+ u32 m_width;
+ u32 m_height;
+};
+
+OpenGLDisplayWindow::OpenGLDisplayWindow(QWindow* parent) : QWindow(parent)
+{
+ setSurfaceType(QWindow::OpenGLSurface);
+}
+
+OpenGLDisplayWindow::~OpenGLDisplayWindow() = default;
+
+HostDisplay::RenderAPI OpenGLDisplayWindow::GetRenderAPI() const
+{
+ return HostDisplay::RenderAPI::OpenGL;
+}
+
+void* OpenGLDisplayWindow::GetRenderDevice() const
+{
+ return nullptr;
+}
+
+void* OpenGLDisplayWindow::GetRenderContext() const
+{
+ return m_gl_context;
+}
+
+void* OpenGLDisplayWindow::GetRenderWindow() const
+{
+ return const_cast(static_cast(this));
+}
+
+void OpenGLDisplayWindow::ChangeRenderWindow(void* new_window)
+{
+ Panic("Not implemented");
+}
+
+std::unique_ptr OpenGLDisplayWindow::CreateTexture(u32 width, u32 height, const void* data,
+ u32 data_stride, bool dynamic)
+{
+ return OpenGLHostDisplayTexture::Create(width, height, data, data_stride);
+}
+
+void OpenGLDisplayWindow::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height,
+ const void* data, u32 data_stride)
+{
+ OpenGLHostDisplayTexture* tex = static_cast(texture);
+ Assert(data_stride == (width * sizeof(u32)));
+
+ GLint old_texture_binding = 0;
+ glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding);
+
+ glBindTexture(GL_TEXTURE_2D, tex->GetGLID());
+ glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
+
+ glBindTexture(GL_TEXTURE_2D, old_texture_binding);
+}
+
+void OpenGLDisplayWindow::SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height,
+ u32 texture_width, u32 texture_height, float aspect_ratio)
+{
+ m_display_texture_id = static_cast(reinterpret_cast(texture));
+ m_display_offset_x = offset_x;
+ m_display_offset_y = offset_y;
+ m_display_width = width;
+ m_display_height = height;
+ m_display_texture_width = texture_width;
+ m_display_texture_height = texture_height;
+ m_display_aspect_ratio = aspect_ratio;
+ m_display_texture_changed = true;
+}
+
+void OpenGLDisplayWindow::SetDisplayLinearFiltering(bool enabled)
+{
+ m_display_linear_filtering = enabled;
+}
+
+void OpenGLDisplayWindow::SetDisplayTopMargin(int height)
+{
+ m_display_top_margin = height;
+}
+
+void OpenGLDisplayWindow::SetVSync(bool enabled)
+{
+ // Window framebuffer has to be bound to call SetSwapInterval.
+ GLint current_fbo = 0;
+ glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤t_fbo);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+ // SDL_GL_SetSwapInterval(enabled ? 1 : 0);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, current_fbo);
+}
+
+std::tuple OpenGLDisplayWindow::GetWindowSize() const
+{
+ const QSize s = size();
+ return std::make_tuple(static_cast(s.width()), static_cast(s.height()));
+}
+
+void OpenGLDisplayWindow::WindowResized() {}
+
+const char* OpenGLDisplayWindow::GetGLSLVersionString() const
+{
+ return m_is_gles ? "#version 300 es" : "#version 130\n";
+}
+
+std::string OpenGLDisplayWindow::GetGLSLVersionHeader() const
+{
+ std::string header = GetGLSLVersionString();
+ header += "\n\n";
+ if (m_is_gles)
+ {
+ header += "precision highp float;\n";
+ header += "precision highp int;\n\n";
+ }
+
+ return header;
+}
+
+static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
+ const GLchar* message, const void* userParam)
+{
+ switch (severity)
+ {
+ case GL_DEBUG_SEVERITY_HIGH_KHR:
+ Log_ErrorPrintf(message);
+ break;
+ case GL_DEBUG_SEVERITY_MEDIUM_KHR:
+ Log_WarningPrint(message);
+ break;
+ case GL_DEBUG_SEVERITY_LOW_KHR:
+ Log_InfoPrintf(message);
+ break;
+ case GL_DEBUG_SEVERITY_NOTIFICATION:
+ // Log_DebugPrint(message);
+ break;
+ }
+}
+
+bool OpenGLDisplayWindow::createGLContext(QThread* worker_thread)
+{
+ m_gl_context = new QOpenGLContext();
+
+ // Prefer a desktop OpenGL context where possible. If we can't get this, try OpenGL ES.
+ static constexpr std::array, 11> desktop_versions_to_try = {
+ {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3}, {3, 2}, {3, 1}, {3, 0}}};
+ static constexpr std::array, 4> es_versions_to_try = {{{3, 2}, {3, 1}, {3, 0}}};
+
+ QSurfaceFormat surface_format = requestedFormat();
+ surface_format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
+ surface_format.setSwapInterval(0);
+ surface_format.setRenderableType(QSurfaceFormat::OpenGL);
+ surface_format.setProfile(QSurfaceFormat::CoreProfile);
+
+#ifdef _DEBUG
+ surface_format.setOption(QSurfaceFormat::DebugContext);
+#endif
+
+ for (const auto [major, minor] : desktop_versions_to_try)
+ {
+ surface_format.setVersion(major, minor);
+ m_gl_context->setFormat(surface_format);
+ if (m_gl_context->create())
+ {
+ Log_InfoPrintf("Got a desktop OpenGL %d.%d context", major, minor);
+ break;
+ }
+ }
+
+ if (!m_gl_context)
+ {
+ // try es
+ surface_format.setRenderableType(QSurfaceFormat::OpenGLES);
+ surface_format.setProfile(QSurfaceFormat::NoProfile);
+#ifdef _DEBUG
+ surface_format.setOption(QSurfaceFormat::DebugContext, false);
+#endif
+
+ for (const auto [major, minor] : es_versions_to_try)
+ {
+ surface_format.setVersion(major, minor);
+ m_gl_context->setFormat(surface_format);
+ if (m_gl_context->create())
+ {
+ Log_InfoPrintf("Got a OpenGL ES %d.%d context", major, minor);
+ m_is_gles = true;
+ break;
+ }
+ }
+ }
+
+ if (!m_gl_context->isValid())
+ {
+ Log_ErrorPrintf("Failed to create any GL context");
+ delete m_gl_context;
+ m_gl_context = nullptr;
+ return false;
+ }
+
+ if (!m_gl_context->makeCurrent(this))
+ {
+ Log_ErrorPrintf("Failed to make GL context current on UI thread");
+ delete m_gl_context;
+ m_gl_context = nullptr;
+ return false;
+ }
+
+ m_gl_context->doneCurrent();
+ m_gl_context->moveToThread(worker_thread);
+ return true;
+}
+
+bool OpenGLDisplayWindow::initializeGLContext()
+{
+ if (!m_gl_context->makeCurrent(this))
+ return false;
+
+ s_thread_gl_context = m_gl_context;
+
+ // Load GLAD.
+ const auto load_result =
+ m_is_gles ? gladLoadGLES2Loader(GetProcAddressCallback) : gladLoadGLLoader(GetProcAddressCallback);
+ if (!load_result)
+ {
+ Log_ErrorPrintf("Failed to load GL functions");
+ return false;
+ }
+
+#if 1
+ if (GLAD_GL_KHR_debug)
+ {
+ glad_glDebugMessageCallbackKHR(GLDebugCallback, nullptr);
+ glEnable(GL_DEBUG_OUTPUT);
+ glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
+ }
+#endif
+
+ if (!CreateImGuiContext() || !CreateGLResources())
+ {
+ s_thread_gl_context = nullptr;
+ m_gl_context->doneCurrent();
+ return false;
+ }
+
+ return true;
+}
+
+void OpenGLDisplayWindow::destroyGLContext()
+{
+ Assert(m_gl_context && s_thread_gl_context == m_gl_context);
+ s_thread_gl_context = nullptr;
+
+ if (m_display_vao != 0)
+ glDeleteVertexArrays(1, &m_display_vao);
+ if (m_display_linear_sampler != 0)
+ glDeleteSamplers(1, &m_display_linear_sampler);
+ if (m_display_nearest_sampler != 0)
+ glDeleteSamplers(1, &m_display_nearest_sampler);
+
+ m_display_program.Destroy();
+
+ m_gl_context->doneCurrent();
+ delete m_gl_context;
+ m_gl_context = nullptr;
+}
+
+bool OpenGLDisplayWindow::CreateImGuiContext()
+{
+ return true;
+}
+
+bool OpenGLDisplayWindow::CreateGLResources()
+{
+ static constexpr char fullscreen_quad_vertex_shader[] = R"(
+uniform vec4 u_src_rect;
+out vec2 v_tex0;
+
+void main()
+{
+ vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
+ v_tex0 = u_src_rect.xy + pos * u_src_rect.zw;
+ gl_Position = vec4(pos * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f);
+}
+)";
+
+ static constexpr char display_fragment_shader[] = R"(
+uniform sampler2D samp0;
+
+in vec2 v_tex0;
+out vec4 o_col0;
+
+void main()
+{
+ o_col0 = texture(samp0, v_tex0);
+}
+)";
+
+ if (!m_display_program.Compile(GetGLSLVersionHeader() + fullscreen_quad_vertex_shader,
+ GetGLSLVersionHeader() + display_fragment_shader))
+ {
+ Log_ErrorPrintf("Failed to compile display shaders");
+ return false;
+ }
+
+ if (!m_is_gles)
+ m_display_program.BindFragData(0, "o_col0");
+
+ if (!m_display_program.Link())
+ {
+ Log_ErrorPrintf("Failed to link display program");
+ return false;
+ }
+
+ m_display_program.Bind();
+ m_display_program.RegisterUniform("u_src_rect");
+ m_display_program.RegisterUniform("samp0");
+ m_display_program.Uniform1i(1, 0);
+
+ glGenVertexArrays(1, &m_display_vao);
+
+ // samplers
+ glGenSamplers(1, &m_display_nearest_sampler);
+ glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glGenSamplers(1, &m_display_linear_sampler);
+ glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ return true;
+}
+
+void OpenGLDisplayWindow::Render()
+{
+ glDisable(GL_SCISSOR_TEST);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+ glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ RenderDisplay();
+
+ // ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+
+ m_gl_context->makeCurrent(this);
+ m_gl_context->swapBuffers(this);
+
+ // ImGui_ImplSDL2_NewFrame(m_window);
+ // ImGui_ImplOpenGL3_NewFrame();
+
+ GL::Program::ResetLastProgram();
+}
+
+void OpenGLDisplayWindow::RenderDisplay()
+{
+ if (!m_display_texture_id)
+ return;
+
+ // - 20 for main menu padding
+ const QSize window_size = size();
+ const auto [vp_left, vp_top, vp_width, vp_height] = CalculateDrawRect(
+ window_size.width(), std::max(window_size.height() - m_display_top_margin, 1), m_display_aspect_ratio);
+
+ glViewport(vp_left, window_size.height() - (m_display_top_margin + vp_top) - vp_height, vp_width, vp_height);
+ glDisable(GL_BLEND);
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_SCISSOR_TEST);
+ glDepthMask(GL_FALSE);
+ m_display_program.Bind();
+ m_display_program.Uniform4f(0, static_cast(m_display_offset_x) / static_cast(m_display_texture_width),
+ static_cast(m_display_offset_y) / static_cast(m_display_texture_height),
+ static_cast(m_display_width) / static_cast(m_display_texture_width),
+ static_cast(m_display_height) / static_cast(m_display_texture_height));
+ glBindTexture(GL_TEXTURE_2D, m_display_texture_id);
+ glBindSampler(0, m_display_linear_filtering ? m_display_linear_sampler : m_display_nearest_sampler);
+ glBindVertexArray(m_display_vao);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ glBindSampler(0, 0);
+}
diff --git a/src/duckstation-qt/opengldisplaywindow.h b/src/duckstation-qt/opengldisplaywindow.h
new file mode 100644
index 000000000..0749fb2a3
--- /dev/null
+++ b/src/duckstation-qt/opengldisplaywindow.h
@@ -0,0 +1,75 @@
+#pragma once
+#include
+
+#include
+#include
+#include "common/gl/program.h"
+#include "common/gl/texture.h"
+#include "core/host_display.h"
+#include
+#include
+
+class OpenGLDisplayWindow final : public QWindow, public HostDisplay
+{
+ Q_OBJECT
+
+public:
+ explicit OpenGLDisplayWindow(QWindow* parent);
+ ~OpenGLDisplayWindow();
+
+ bool createGLContext(QThread* worker_thread);
+ bool initializeGLContext();
+ void destroyGLContext();
+
+ RenderAPI GetRenderAPI() const override;
+ void* GetRenderDevice() const override;
+ void* GetRenderContext() const override;
+ void* GetRenderWindow() const override;
+
+ void ChangeRenderWindow(void* new_window) override;
+
+ std::unique_ptr CreateTexture(u32 width, u32 height, const void* data, u32 data_stride,
+ bool dynamic) override;
+ void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data,
+ u32 data_stride) override;
+
+ void SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height, u32 texture_width,
+ u32 texture_height, float aspect_ratio) override;
+ void SetDisplayLinearFiltering(bool enabled) override;
+ void SetDisplayTopMargin(int height) override;
+
+ void SetVSync(bool enabled) override;
+
+ std::tuple GetWindowSize() const override;
+ void WindowResized() override;
+
+private:
+ const char* GetGLSLVersionString() const;
+ std::string GetGLSLVersionHeader() const;
+
+ bool CreateImGuiContext();
+ bool CreateGLResources();
+
+ void Render();
+ void RenderDisplay();
+
+ QOpenGLContext* m_gl_context = nullptr;
+
+ GL::Program m_display_program;
+ GLuint m_display_vao = 0;
+ GLuint m_display_texture_id = 0;
+ s32 m_display_offset_x = 0;
+ s32 m_display_offset_y = 0;
+ s32 m_display_width = 0;
+ s32 m_display_height = 0;
+ u32 m_display_texture_width = 0;
+ u32 m_display_texture_height = 0;
+ int m_display_top_margin = 0;
+ float m_display_aspect_ratio = 1.0f;
+ GLuint m_display_nearest_sampler = 0;
+ GLuint m_display_linear_sampler = 0;
+
+ bool m_is_gles = false;
+ bool m_display_texture_changed = false;
+ bool m_display_linear_filtering = false;
+};
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
new file mode 100644
index 000000000..c07913da7
--- /dev/null
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -0,0 +1,282 @@
+#include "qthostinterface.h"
+#include "YBaseLib/Log.h"
+#include "common/null_audio_stream.h"
+#include "core/game_list.h"
+#include "core/gpu.h"
+#include "core/system.h"
+#include "qtsettingsinterface.h"
+#include
+#include
+#include
+Log_SetChannel(QtHostInterface);
+
+QtHostInterface::QtHostInterface(QObject* parent)
+ : QObject(parent), m_qsettings("duckstation-qt.ini", QSettings::IniFormat)
+{
+ checkSettings();
+ createGameList();
+ createThread();
+}
+
+QtHostInterface::~QtHostInterface()
+{
+ Assert(!m_opengl_display_window);
+ stopThread();
+}
+
+void QtHostInterface::ReportError(const char* message)
+{
+ // QMessageBox::critical(nullptr, tr("DuckStation Error"), message, QMessageBox::Ok);
+}
+
+void QtHostInterface::ReportMessage(const char* message)
+{
+ // QMessageBox::information(nullptr, tr("DuckStation Information"), message, QMessageBox::Ok);
+}
+
+void QtHostInterface::setDefaultSettings()
+{
+ QtSettingsInterface si(m_qsettings);
+ m_settings.SetDefaults();
+ m_settings.Save(si);
+ m_qsettings.sync();
+}
+
+void QtHostInterface::applySettings()
+{
+ QtSettingsInterface si(m_qsettings);
+ m_settings.Load(si);
+}
+
+void QtHostInterface::checkSettings()
+{
+ const QSettings::Status settings_status = m_qsettings.status();
+ if (settings_status != QSettings::NoError)
+ m_qsettings.clear();
+
+ const QString settings_version_key = QStringLiteral("General/SettingsVersion");
+ const int expected_version = 1;
+ const QVariant settings_version_var = m_qsettings.value(settings_version_key);
+ bool settings_version_okay;
+ int settings_version = settings_version_var.toInt(&settings_version_okay);
+ if (!settings_version_okay)
+ settings_version = 0;
+ if (settings_version != expected_version)
+ {
+ Log_WarningPrintf("Settings version %d does not match expected version %d, resetting", settings_version,
+ expected_version);
+ m_qsettings.clear();
+ m_qsettings.setValue(settings_version_key, expected_version);
+ setDefaultSettings();
+ }
+}
+
+void QtHostInterface::createGameList()
+{
+ m_game_list = std::make_unique();
+ updateGameListDatabase(false);
+ refreshGameList(false);
+}
+
+void QtHostInterface::updateGameListDatabase(bool refresh_list /*= true*/)
+{
+ m_game_list->ClearDatabase();
+
+ const QString redump_dat_path = m_qsettings.value("GameList/RedumpDatabasePath").toString();
+ if (!redump_dat_path.isEmpty())
+ m_game_list->ParseRedumpDatabase(redump_dat_path.toStdString().c_str());
+
+ if (refresh_list)
+ refreshGameList(true);
+}
+
+void QtHostInterface::refreshGameList(bool invalidate_cache /*= false*/)
+{
+ QtSettingsInterface si(m_qsettings);
+ m_game_list->SetDirectoriesFromSettings(si);
+ m_game_list->RescanAllDirectories();
+ emit gameListRefreshed();
+}
+
+QWidget* QtHostInterface::createDisplayWidget(QWidget* parent)
+{
+ m_opengl_display_window = new OpenGLDisplayWindow(nullptr);
+ m_display.release();
+ m_display = std::unique_ptr(static_cast(m_opengl_display_window));
+ return QWidget::createWindowContainer(m_opengl_display_window, parent);
+}
+
+void QtHostInterface::destroyDisplayWidget()
+{
+ m_display.release();
+ delete m_opengl_display_window;
+ m_opengl_display_window = nullptr;
+}
+
+void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_state_filename)
+{
+ emit emulationStarting();
+
+ if (!m_opengl_display_window->createGLContext(m_worker_thread))
+ {
+ emit emulationStopped();
+ return;
+ }
+
+ QMetaObject::invokeMethod(this, "doBootSystem", Qt::QueuedConnection, Q_ARG(QString, initial_filename),
+ Q_ARG(QString, initial_save_state_filename));
+}
+
+void QtHostInterface::powerOffSystem()
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "doPowerOffSystem", Qt::QueuedConnection);
+ return;
+ }
+
+ if (!m_system)
+ {
+ Log_ErrorPrintf("powerOffSystem() called without system");
+ return;
+ }
+
+ m_system.reset();
+ m_opengl_display_window->destroyGLContext();
+
+ emit emulationStopped();
+}
+
+void QtHostInterface::resetSystem()
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "resetSystem", Qt::QueuedConnection);
+ return;
+ }
+
+ if (!m_system)
+ {
+ Log_ErrorPrintf("resetSystem() called without system");
+ return;
+ }
+
+ HostInterface::ResetSystem();
+}
+
+void QtHostInterface::pauseSystem(bool paused)
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "pauseSystem", Qt::QueuedConnection, Q_ARG(bool, paused));
+ return;
+ }
+
+ m_paused = paused;
+ emit emulationPaused(paused);
+}
+
+void QtHostInterface::changeDisc(QString new_disc_filename) {}
+
+void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename)
+{
+ if (!m_opengl_display_window->initializeGLContext())
+ {
+ emit emulationStopped();
+ return;
+ }
+
+ m_audio_stream = NullAudioStream::Create();
+ m_audio_stream->Reconfigure();
+
+ std::string initial_filename_str = initial_filename.toStdString();
+ std::string initial_save_state_filename_str = initial_save_state_filename.toStdString();
+ if (!CreateSystem() ||
+ !BootSystem(initial_filename_str.empty() ? nullptr : initial_filename_str.c_str(),
+ initial_save_state_filename_str.empty() ? nullptr : initial_save_state_filename_str.c_str()))
+ {
+ m_opengl_display_window->destroyGLContext();
+ emit emulationStopped();
+ return;
+ }
+
+ emit emulationStarted();
+}
+
+void QtHostInterface::createThread()
+{
+ m_original_thread = QThread::currentThread();
+ m_worker_thread = new Thread(this);
+ m_worker_thread->start();
+ moveToThread(m_worker_thread);
+}
+
+void QtHostInterface::stopThread()
+{
+ Assert(!isOnWorkerThread());
+
+ QMetaObject::invokeMethod(this, "doStopThread", Qt::QueuedConnection);
+ m_worker_thread->wait();
+}
+
+void QtHostInterface::doStopThread()
+{
+ m_shutdown_flag.store(true);
+}
+
+void QtHostInterface::threadEntryPoint()
+{
+ while (!m_shutdown_flag.load())
+ {
+ if (!m_system)
+ {
+ // wait until we have a system before running
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 1000);
+ continue;
+ }
+
+ // execute the system, polling events inbetween frames
+ // simulate the system if not paused
+ if (m_system && !m_paused)
+ m_system->RunFrame();
+
+ // rendering
+ {
+ // DrawImGui();
+
+ if (m_system)
+ m_system->GetGPU()->ResetGraphicsAPIState();
+
+ // ImGui::Render();
+ m_display->Render();
+
+ // ImGui::NewFrame();
+
+ if (m_system)
+ {
+ m_system->GetGPU()->RestoreGraphicsAPIState();
+
+ if (m_speed_limiter_enabled)
+ Throttle();
+ }
+
+ UpdatePerformanceCounters();
+ }
+
+ QCoreApplication::processEvents(QEventLoop::AllEvents, m_paused ? 16 : 0);
+ }
+
+ m_system.reset();
+
+ // move back to UI thread
+ moveToThread(m_original_thread);
+}
+
+QtHostInterface::Thread::Thread(QtHostInterface* parent) : QThread(parent), m_parent(parent) {}
+
+QtHostInterface::Thread::~Thread() = default;
+
+void QtHostInterface::Thread::run()
+{
+ m_parent->threadEntryPoint();
+}
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
new file mode 100644
index 000000000..37ebfe80c
--- /dev/null
+++ b/src/duckstation-qt/qthostinterface.h
@@ -0,0 +1,89 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include "core/host_interface.h"
+#include "opengldisplaywindow.h"
+
+class QWidget;
+
+class GameList;
+
+class QtHostInterface : public QObject, private HostInterface
+{
+ Q_OBJECT
+
+public:
+ explicit QtHostInterface(QObject* parent = nullptr);
+ ~QtHostInterface();
+
+ void ReportError(const char* message) override;
+ void ReportMessage(const char* message) override;
+
+ const QSettings& getQSettings() const { return m_qsettings; }
+ QSettings& getQSettings() { return m_qsettings; }
+ void setDefaultSettings();
+ void applySettings();
+
+ const GameList* getGameList() const { return m_game_list.get(); }
+ GameList* getGameList() { return m_game_list.get(); }
+ void updateGameListDatabase(bool refresh_list = true);
+ void refreshGameList(bool invalidate_cache = false);
+
+ bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; }
+
+ QWidget* createDisplayWidget(QWidget* parent);
+ void destroyDisplayWidget();
+
+ void bootSystem(QString initial_filename, QString initial_save_state_filename);
+
+Q_SIGNALS:
+ void emulationStarting();
+ void emulationStarted();
+ void emulationStopped();
+ void emulationPaused(bool paused);
+ void gameListRefreshed();
+
+public Q_SLOTS:
+ void powerOffSystem();
+ void resetSystem();
+ void pauseSystem(bool paused);
+ void changeDisc(QString new_disc_filename);
+
+private Q_SLOTS:
+ void doBootSystem(QString initial_filename, QString initial_save_state_filename);
+ void doStopThread();
+
+private:
+ class Thread : public QThread
+ {
+ public:
+ Thread(QtHostInterface* parent);
+ ~Thread();
+
+ protected:
+ void run() override;
+
+ private:
+ QtHostInterface* m_parent;
+ };
+
+ void checkSettings();
+ void createGameList();
+ void createThread();
+ void stopThread();
+ void threadEntryPoint();
+
+ QSettings m_qsettings;
+
+ std::unique_ptr m_game_list;
+
+ OpenGLDisplayWindow* m_opengl_display_window = nullptr;
+ QThread* m_original_thread = nullptr;
+ Thread* m_worker_thread = nullptr;
+
+ std::atomic_bool m_shutdown_flag{ false };
+};
+
diff --git a/src/duckstation-qt/qtsettingsinterface.cpp b/src/duckstation-qt/qtsettingsinterface.cpp
new file mode 100644
index 000000000..a9843da63
--- /dev/null
+++ b/src/duckstation-qt/qtsettingsinterface.cpp
@@ -0,0 +1,142 @@
+#include "qtsettingsinterface.h"
+#include
+#include
+
+static QString GetFullKey(const char* section, const char* key)
+{
+ return QStringLiteral("%1/%2").arg(section, key);
+}
+
+QtSettingsInterface::QtSettingsInterface(QSettings& settings) : m_settings(settings) {}
+
+QtSettingsInterface::~QtSettingsInterface() = default;
+
+int QtSettingsInterface::GetIntValue(const char* section, const char* key, int default_value /*= 0*/)
+{
+ QVariant value = m_settings.value(GetFullKey(section, key));
+ if (!value.isValid())
+ return default_value;
+
+ bool converted_value_okay;
+ int converted_value = value.toInt(&converted_value_okay);
+ if (!converted_value_okay)
+ return default_value;
+ else
+ return converted_value;
+}
+
+float QtSettingsInterface::GetFloatValue(const char* section, const char* key, float default_value /*= 0.0f*/)
+{
+ QVariant value = m_settings.value(GetFullKey(section, key));
+ if (!value.isValid())
+ return default_value;
+
+ bool converted_value_okay;
+ float converted_value = value.toFloat(&converted_value_okay);
+ if (!converted_value_okay)
+ return default_value;
+ else
+ return converted_value;
+}
+
+bool QtSettingsInterface::GetBoolValue(const char* section, const char* key, bool default_value /*= false*/)
+{
+ QVariant value = m_settings.value(GetFullKey(section, key));
+ return value.isValid() ? value.toBool() : default_value;
+}
+
+std::string QtSettingsInterface::GetStringValue(const char* section, const char* key,
+ const char* default_value /*= ""*/)
+{
+ QVariant value = m_settings.value(GetFullKey(section, key));
+ return value.isValid() ? value.toString().toStdString() : std::string(default_value);
+}
+
+void QtSettingsInterface::SetIntValue(const char* section, const char* key, int value)
+{
+ m_settings.setValue(GetFullKey(section, key), QVariant(value));
+}
+
+void QtSettingsInterface::SetFloatValue(const char* section, const char* key, float value)
+{
+ m_settings.setValue(GetFullKey(section, key), QVariant(value));
+}
+
+void QtSettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
+{
+ m_settings.setValue(GetFullKey(section, key), QVariant(value));
+}
+
+void QtSettingsInterface::SetStringValue(const char* section, const char* key, const char* value)
+{
+ m_settings.setValue(GetFullKey(section, key), QVariant(value));
+}
+
+std::vector QtSettingsInterface::GetStringList(const char* section, const char* key)
+{
+ QVariant value = m_settings.value(GetFullKey(section, key));
+ if (value.type() == QVariant::String)
+ return { value.toString().toStdString() };
+ else if (value.type() != QVariant::StringList)
+ return {};
+
+ QStringList value_sl = value.toStringList();
+ std::vector results;
+ results.reserve(static_cast(value_sl.size()));
+ std::transform(value_sl.begin(), value_sl.end(), std::back_inserter(results),
+ [](const QString& str) { return str.toStdString(); });
+ return results;
+}
+
+void QtSettingsInterface::SetStringList(const char* section, const char* key,
+ const std::vector& items)
+{
+ QString full_key = GetFullKey(section, key);
+ if (items.empty())
+ {
+ m_settings.remove(full_key);
+ return;
+ }
+
+ QStringList sl;
+ sl.reserve(static_cast(items.size()));
+ std::transform(items.begin(), items.end(), std::back_inserter(sl), [](const std::string_view& sv) {
+ return QString::fromLocal8Bit(sv.data(), static_cast(sv.size()));
+ });
+ m_settings.setValue(full_key, sl);
+}
+
+bool QtSettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
+{
+ QString full_key = GetFullKey(section, key);
+ QVariant var = m_settings.value(full_key);
+ QStringList sl = var.toStringList();
+ if (sl.removeAll(item) == 0)
+ return false;
+
+ if (sl.isEmpty())
+ m_settings.remove(full_key);
+ else
+ m_settings.setValue(full_key, sl);
+ return true;
+}
+
+bool QtSettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
+{
+ QString full_key = GetFullKey(section, key);
+ QVariant var = m_settings.value(full_key);
+
+ QStringList sl = (var.type() == QVariant::StringList) ? var.toStringList() : QStringList();
+ QString qitem(item);
+ if (sl.contains(qitem))
+ return false;
+
+ sl.push_back(qitem);
+ m_settings.setValue(full_key, sl);
+ return true;
+}
+
+void QtSettingsInterface::DeleteValue(const char* section, const char* key)
+{
+ m_settings.remove(GetFullKey(section, key));
+}
diff --git a/src/duckstation-qt/qtsettingsinterface.h b/src/duckstation-qt/qtsettingsinterface.h
new file mode 100644
index 000000000..15851e334
--- /dev/null
+++ b/src/duckstation-qt/qtsettingsinterface.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "core/settings.h"
+
+class QSettings;
+
+class QtSettingsInterface : public SettingsInterface
+{
+public:
+ QtSettingsInterface(QSettings& settings);
+ ~QtSettingsInterface();
+
+ int GetIntValue(const char* section, const char* key, int default_value = 0) override;
+ float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) override;
+ bool GetBoolValue(const char* section, const char* key, bool default_value = false) override;
+ std::string GetStringValue(const char* section, const char* key, const char* default_value = "") override;
+
+ void SetIntValue(const char* section, const char* key, int value) override;
+ void SetFloatValue(const char* section, const char* key, float value) override;
+ void SetBoolValue(const char* section, const char* key, bool value) override;
+ void SetStringValue(const char* section, const char* key, const char* value) override;
+
+ std::vector GetStringList(const char* section, const char* key) override;
+ void SetStringList(const char* section, const char* key, const std::vector& items) override;
+ bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
+ bool AddToStringList(const char* section, const char* key, const char* item) override;
+
+ void DeleteValue(const char* section, const char* key) override;
+
+private:
+ QSettings& m_settings;
+};
\ No newline at end of file
diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp
new file mode 100644
index 000000000..a1295d784
--- /dev/null
+++ b/src/duckstation-qt/qtutils.cpp
@@ -0,0 +1,23 @@
+#include "qtutils.h"
+#include
+#include
+
+namespace QtUtils {
+
+void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths)
+{
+ const int total_width =
+ std::accumulate(widths.begin(), widths.end(), 0, [](int a, int b) { return a + std::max(b, 0); });
+
+ const int flex_width = std::max(view->width() - total_width - 2, 1);
+
+ int column_index = 0;
+ for (const int spec_width : widths)
+ {
+ const int width = spec_width < 0 ? flex_width : spec_width;
+ view->setColumnWidth(column_index, width);
+ column_index++;
+ }
+}
+
+} // namespace QtUtils
\ No newline at end of file
diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h
new file mode 100644
index 000000000..ea2b6f85a
--- /dev/null
+++ b/src/duckstation-qt/qtutils.h
@@ -0,0 +1,12 @@
+#pragma once
+#include
+
+class QTableView;
+
+namespace QtUtils {
+
+/// Resizes columns of the table view to at the specified widths. A width of -1 will stretch the column to use the
+/// remaining space.
+void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths);
+
+} // namespace QtUtils
\ No newline at end of file
diff --git a/src/duckstation-qt/resources/icons.qrc b/src/duckstation-qt/resources/icons.qrc
new file mode 100644
index 000000000..f461499bf
--- /dev/null
+++ b/src/duckstation-qt/resources/icons.qrc
@@ -0,0 +1,29 @@
+
+
+ icons/applications-internet.png
+ icons/system-search.png
+ icons/list-add.png
+ icons/list-remove.png
+ icons/duck.png
+ icons/edit-find.png
+ icons/folder-open.png
+ icons/applications-development.png
+ icons/applications-other.png
+ icons/applications-system.png
+ icons/audio-card.png
+ icons/document-open.png
+ icons/document-save.png
+ icons/drive-optical.png
+ icons/drive-removable-media.png
+ icons/input-gaming.png
+ icons/media-flash.png
+ icons/media-optical.png
+ icons/media-playback-pause.png
+ icons/media-playback-start.png
+ icons/system-shutdown.png
+ icons/utilities-system-monitor.png
+ icons/video-display.png
+ icons/view-fullscreen.png
+ icons/view-refresh.png
+
+
diff --git a/src/duckstation-qt/resources/icons/applications-development.png b/src/duckstation-qt/resources/icons/applications-development.png
new file mode 100644
index 000000000..bc88a5c56
Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-development.png differ
diff --git a/src/duckstation-qt/resources/icons/applications-internet.png b/src/duckstation-qt/resources/icons/applications-internet.png
new file mode 100644
index 000000000..096e84895
Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-internet.png differ
diff --git a/src/duckstation-qt/resources/icons/applications-other.png b/src/duckstation-qt/resources/icons/applications-other.png
new file mode 100644
index 000000000..1990dbb82
Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-other.png differ
diff --git a/src/duckstation-qt/resources/icons/applications-system.png b/src/duckstation-qt/resources/icons/applications-system.png
new file mode 100644
index 000000000..565f406dd
Binary files /dev/null and b/src/duckstation-qt/resources/icons/applications-system.png differ
diff --git a/src/duckstation-qt/resources/icons/audio-card.png b/src/duckstation-qt/resources/icons/audio-card.png
new file mode 100644
index 000000000..5b15dd6d2
Binary files /dev/null and b/src/duckstation-qt/resources/icons/audio-card.png differ
diff --git a/src/duckstation-qt/resources/icons/document-open.png b/src/duckstation-qt/resources/icons/document-open.png
new file mode 100644
index 000000000..f35f25835
Binary files /dev/null and b/src/duckstation-qt/resources/icons/document-open.png differ
diff --git a/src/duckstation-qt/resources/icons/document-save.png b/src/duckstation-qt/resources/icons/document-save.png
new file mode 100644
index 000000000..db5c52b76
Binary files /dev/null and b/src/duckstation-qt/resources/icons/document-save.png differ
diff --git a/src/duckstation-qt/resources/icons/drive-optical.png b/src/duckstation-qt/resources/icons/drive-optical.png
new file mode 100644
index 000000000..bf2e8c89f
Binary files /dev/null and b/src/duckstation-qt/resources/icons/drive-optical.png differ
diff --git a/src/duckstation-qt/resources/icons/drive-removable-media.png b/src/duckstation-qt/resources/icons/drive-removable-media.png
new file mode 100644
index 000000000..2d2890935
Binary files /dev/null and b/src/duckstation-qt/resources/icons/drive-removable-media.png differ
diff --git a/src/duckstation-qt/resources/icons/duck.png b/src/duckstation-qt/resources/icons/duck.png
new file mode 100644
index 000000000..c78869e43
Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck.png differ
diff --git a/src/duckstation-qt/resources/icons/duck_128.png b/src/duckstation-qt/resources/icons/duck_128.png
new file mode 100644
index 000000000..bc6655bbd
Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck_128.png differ
diff --git a/src/duckstation-qt/resources/icons/duck_64.png b/src/duckstation-qt/resources/icons/duck_64.png
new file mode 100644
index 000000000..82b5aaa5d
Binary files /dev/null and b/src/duckstation-qt/resources/icons/duck_64.png differ
diff --git a/src/duckstation-qt/resources/icons/edit-find.png b/src/duckstation-qt/resources/icons/edit-find.png
new file mode 100644
index 000000000..5594785d1
Binary files /dev/null and b/src/duckstation-qt/resources/icons/edit-find.png differ
diff --git a/src/duckstation-qt/resources/icons/folder-open.png b/src/duckstation-qt/resources/icons/folder-open.png
new file mode 100644
index 000000000..901816c8e
Binary files /dev/null and b/src/duckstation-qt/resources/icons/folder-open.png differ
diff --git a/src/duckstation-qt/resources/icons/input-gaming.png b/src/duckstation-qt/resources/icons/input-gaming.png
new file mode 100644
index 000000000..26e2a9827
Binary files /dev/null and b/src/duckstation-qt/resources/icons/input-gaming.png differ
diff --git a/src/duckstation-qt/resources/icons/list-add.png b/src/duckstation-qt/resources/icons/list-add.png
new file mode 100644
index 000000000..2acdd8f51
Binary files /dev/null and b/src/duckstation-qt/resources/icons/list-add.png differ
diff --git a/src/duckstation-qt/resources/icons/list-remove.png b/src/duckstation-qt/resources/icons/list-remove.png
new file mode 100644
index 000000000..c5524f728
Binary files /dev/null and b/src/duckstation-qt/resources/icons/list-remove.png differ
diff --git a/src/duckstation-qt/resources/icons/media-flash.png b/src/duckstation-qt/resources/icons/media-flash.png
new file mode 100644
index 000000000..7540f3fd2
Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-flash.png differ
diff --git a/src/duckstation-qt/resources/icons/media-optical.png b/src/duckstation-qt/resources/icons/media-optical.png
new file mode 100644
index 000000000..5853a754e
Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-optical.png differ
diff --git a/src/duckstation-qt/resources/icons/media-playback-pause.png b/src/duckstation-qt/resources/icons/media-playback-pause.png
new file mode 100644
index 000000000..1e9f4d535
Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-playback-pause.png differ
diff --git a/src/duckstation-qt/resources/icons/media-playback-start.png b/src/duckstation-qt/resources/icons/media-playback-start.png
new file mode 100644
index 000000000..66f32d89b
Binary files /dev/null and b/src/duckstation-qt/resources/icons/media-playback-start.png differ
diff --git a/src/duckstation-qt/resources/icons/system-search.png b/src/duckstation-qt/resources/icons/system-search.png
new file mode 100644
index 000000000..950d792af
Binary files /dev/null and b/src/duckstation-qt/resources/icons/system-search.png differ
diff --git a/src/duckstation-qt/resources/icons/system-shutdown.png b/src/duckstation-qt/resources/icons/system-shutdown.png
new file mode 100644
index 000000000..36acd46bd
Binary files /dev/null and b/src/duckstation-qt/resources/icons/system-shutdown.png differ
diff --git a/src/duckstation-qt/resources/icons/utilities-system-monitor.png b/src/duckstation-qt/resources/icons/utilities-system-monitor.png
new file mode 100644
index 000000000..b62959e4f
Binary files /dev/null and b/src/duckstation-qt/resources/icons/utilities-system-monitor.png differ
diff --git a/src/duckstation-qt/resources/icons/video-display.png b/src/duckstation-qt/resources/icons/video-display.png
new file mode 100644
index 000000000..b95ea5d36
Binary files /dev/null and b/src/duckstation-qt/resources/icons/video-display.png differ
diff --git a/src/duckstation-qt/resources/icons/view-fullscreen.png b/src/duckstation-qt/resources/icons/view-fullscreen.png
new file mode 100644
index 000000000..00e6b83cc
Binary files /dev/null and b/src/duckstation-qt/resources/icons/view-fullscreen.png differ
diff --git a/src/duckstation-qt/resources/icons/view-refresh.png b/src/duckstation-qt/resources/icons/view-refresh.png
new file mode 100644
index 000000000..606ea9eba
Binary files /dev/null and b/src/duckstation-qt/resources/icons/view-refresh.png differ
diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp
new file mode 100644
index 000000000..3f219bb34
--- /dev/null
+++ b/src/duckstation-qt/settingsdialog.cpp
@@ -0,0 +1,42 @@
+#include "settingsdialog.h"
+#include "consolesettingswidget.h"
+#include "gamelistsettingswidget.h"
+#include "qthostinterface.h"
+#include
+
+SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
+ : QDialog(parent), m_host_interface(host_interface)
+{
+ m_ui.setupUi(this);
+
+ m_console_settings = new ConsoleSettingsWidget(m_ui.settingsContainer);
+ m_game_list_settings = new GameListSettingsWidget(host_interface, m_ui.settingsContainer);
+ m_cpu_settings = new QWidget(m_ui.settingsContainer);
+ m_gpu_settings = new QWidget(m_ui.settingsContainer);
+ m_audio_settings = new QWidget(m_ui.settingsContainer);
+
+ m_ui.settingsContainer->insertWidget(0, m_console_settings);
+ m_ui.settingsContainer->insertWidget(1, m_game_list_settings);
+ m_ui.settingsContainer->insertWidget(2, m_cpu_settings);
+ m_ui.settingsContainer->insertWidget(3, m_gpu_settings);
+ m_ui.settingsContainer->insertWidget(4, m_audio_settings);
+
+ m_ui.settingsCategory->setCurrentRow(0);
+ m_ui.settingsContainer->setCurrentIndex(0);
+ connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this, &SettingsDialog::onCategoryCurrentRowChanged);
+}
+
+SettingsDialog::~SettingsDialog() = default;
+
+void SettingsDialog::setCategory(Category category)
+{
+ if (category >= Category::Count)
+ return;
+
+ m_ui.settingsCategory->setCurrentRow(static_cast(category));
+}
+
+void SettingsDialog::onCategoryCurrentRowChanged(int row)
+{
+ m_ui.settingsContainer->setCurrentIndex(row);
+}
diff --git a/src/duckstation-qt/settingsdialog.h b/src/duckstation-qt/settingsdialog.h
new file mode 100644
index 000000000..f006768cb
--- /dev/null
+++ b/src/duckstation-qt/settingsdialog.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+#include "ui_settingsdialog.h"
+
+class QtHostInterface;
+
+class ConsoleSettingsWidget;
+
+class SettingsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ enum class Category
+ {
+ ConsoleSettings,
+ GameListSettings,
+ CPUSettings,
+ GPUSettings,
+ AudioSettings,
+ Count
+ };
+
+ explicit SettingsDialog(QtHostInterface* host_interface, QWidget* parent = nullptr);
+ ~SettingsDialog();
+
+public Q_SLOTS:
+ void setCategory(Category category);
+
+private Q_SLOTS:
+ void onCategoryCurrentRowChanged(int row);
+
+private:
+ Ui::SettingsDialog m_ui;
+
+ QtHostInterface* m_host_interface;
+
+ ConsoleSettingsWidget* m_console_settings = nullptr;
+ QWidget* m_game_list_settings = nullptr;
+ QWidget* m_cpu_settings = nullptr;
+ QWidget* m_gpu_settings = nullptr;
+ QWidget* m_audio_settings = nullptr;
+};
diff --git a/src/duckstation-qt/settingsdialog.ui b/src/duckstation-qt/settingsdialog.ui
new file mode 100644
index 000000000..2e33176a7
--- /dev/null
+++ b/src/duckstation-qt/settingsdialog.ui
@@ -0,0 +1,150 @@
+
+
+ SettingsDialog
+
+
+ Qt::WindowModal
+
+
+
+ 0
+ 0
+ 637
+ 405
+
+
+
+ DuckStation Settings
+
+
+
+ :/icons/applications-system.png:/icons/applications-system.png
+
+
+ -
+
+
+ 10
+
+
-
+
+
+
+ 160
+ 16777215
+
+
+
+
+ 32
+ 32
+
+
+
-
+
+ Console Settings
+
+
+
+ :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png
+
+
+ -
+
+ Game List Settings
+
+
+
+ :/icons/folder-open.png:/icons/folder-open.png
+
+
+ -
+
+ CPU Settings
+
+
+
+ :/icons/applications-other.png:/icons/applications-other.png
+
+
+ -
+
+ GPU Settings
+
+
+
+ :/icons/video-display.png:/icons/video-display.png
+
+
+ -
+
+ Audio Settings
+
+
+
+ :/icons/audio-card.png:/icons/audio-card.png
+
+
+
+
+ -
+
+
+ 0
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults
+
+
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ SettingsDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ SettingsDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+