diff --git a/CMakeLists.txt b/CMakeLists.txt
index d3ca834f..33faba5f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -280,6 +280,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp
                  src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp
                  src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp
                  src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp src/renderdoc.cpp
+                 src/frontend_settings.cpp
 )
 set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp)
 set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp
@@ -361,7 +362,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
                  include/PICA/pica_vert_config.hpp include/sdl_sensors.hpp include/PICA/draw_acceleration.hpp include/renderdoc.hpp
                  include/align.hpp include/audio/aac_decoder.hpp include/PICA/pica_simd.hpp include/services/fonts.hpp
                  include/audio/audio_interpolation.hpp include/audio/hle_mixer.hpp include/audio/dsp_simd.hpp
-                 include/services/dsp_firmware_db.hpp
+                 include/services/dsp_firmware_db.hpp include/frontend_settings.hpp
 )
 
 cmrc_add_resource_library(
@@ -695,6 +696,9 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE)
             PREFIX "/"
             FILES
                 docs/img/rsob_icon.png docs/img/rstarstruck_icon.png docs/img/rpog_icon.png docs/img/rsyn_icon.png
+                docs/img/settings_icon.png docs/img/display_icon.png docs/img/speaker_icon.png
+                docs/img/sparkling_icon.png docs/img/battery_icon.png docs/img/sdcard_icon.png
+                docs/img/rnap_icon.png docs/img/rcow_icon.png
         )
     else()
         set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp)
diff --git a/docs/img/battery_icon.png b/docs/img/battery_icon.png
new file mode 100644
index 00000000..5768a928
Binary files /dev/null and b/docs/img/battery_icon.png differ
diff --git a/docs/img/display_icon.png b/docs/img/display_icon.png
new file mode 100644
index 00000000..cf6a68be
Binary files /dev/null and b/docs/img/display_icon.png differ
diff --git a/docs/img/rcow_icon.png b/docs/img/rcow_icon.png
new file mode 100644
index 00000000..5facb301
Binary files /dev/null and b/docs/img/rcow_icon.png differ
diff --git a/docs/img/rnap_icon.png b/docs/img/rnap_icon.png
new file mode 100644
index 00000000..7967102b
Binary files /dev/null and b/docs/img/rnap_icon.png differ
diff --git a/docs/img/sdcard_icon.png b/docs/img/sdcard_icon.png
new file mode 100644
index 00000000..07ed3fce
Binary files /dev/null and b/docs/img/sdcard_icon.png differ
diff --git a/docs/img/settings_icon.png b/docs/img/settings_icon.png
new file mode 100644
index 00000000..bf21c417
Binary files /dev/null and b/docs/img/settings_icon.png differ
diff --git a/docs/img/sparkling_icon.png b/docs/img/sparkling_icon.png
new file mode 100644
index 00000000..4a46d8d8
Binary files /dev/null and b/docs/img/sparkling_icon.png differ
diff --git a/docs/img/speaker_icon.png b/docs/img/speaker_icon.png
new file mode 100644
index 00000000..06adcfb3
Binary files /dev/null and b/docs/img/speaker_icon.png differ
diff --git a/include/config.hpp b/include/config.hpp
index 5a7c7fff..7361a40d 100644
--- a/include/config.hpp
+++ b/include/config.hpp
@@ -3,6 +3,7 @@
 
 #include "audio/dsp_core.hpp"
 #include "renderer.hpp"
+#include "frontend_settings.hpp"
 
 struct AudioDeviceConfig {
 	float volumeRaw = 1.0f;
@@ -86,8 +87,9 @@ struct EmulatorConfig {
 
 	WindowSettings windowSettings;
 	AudioDeviceConfig audioDeviceConfig;
+	FrontendSettings frontendSettings;
 
 	EmulatorConfig(const std::filesystem::path& path);
 	void load();
 	void save();
-};
+};
\ No newline at end of file
diff --git a/include/discord_rpc.hpp b/include/discord_rpc.hpp
index 9b244faf..62bd0c6b 100644
--- a/include/discord_rpc.hpp
+++ b/include/discord_rpc.hpp
@@ -17,6 +17,8 @@ namespace Discord {
 		void init();
 		void update(RPCStatus status, const std::string& title);
 		void stop();
+
+		bool running() const { return enabled; }
 	};
 }  // namespace Discord
 
diff --git a/include/emulator.hpp b/include/emulator.hpp
index abb74089..cf231328 100644
--- a/include/emulator.hpp
+++ b/include/emulator.hpp
@@ -118,6 +118,9 @@ class Emulator {
 	void setOutputSize(u32 width, u32 height) { gpu.setOutputSize(width, height); }
 	void deinitGraphicsContext() { gpu.deinitGraphicsContext(); }
 
+	// Reloads some settings that require special handling, such as audio enable
+	void reloadSettings();
+
 	EmulatorConfig& getConfig() { return config; }
 	Cheats& getCheats() { return cheats; }
 	ServiceManager& getServiceManager() { return kernel.getServiceManager(); }
diff --git a/include/frontend_settings.hpp b/include/frontend_settings.hpp
new file mode 100644
index 00000000..aaf9eaf0
--- /dev/null
+++ b/include/frontend_settings.hpp
@@ -0,0 +1,32 @@
+#pragma once
+#include <string>
+
+// Some UI settings that aren't fully frontend-dependent. Note: Not all frontends will support the same settings.
+// Note: Any enums should ideally be ordered in the same order we want to show them in UI dropdown menus, so that we can cast indices to enums
+// directly.
+struct FrontendSettings {
+	enum class Theme : int {
+		System = 0,
+		Light = 1,
+		Dark = 2,
+		GreetingsCat = 3,
+		Cream = 4,
+	};
+
+	// Different panda-themed window icons
+	enum class WindowIcon : int {
+		Rpog = 0,
+		Rsyn = 1,
+		Rnap = 2,
+		Rcow = 3,
+	};
+
+	Theme theme = Theme::Dark;
+	WindowIcon icon = WindowIcon::Rpog;
+
+	static Theme themeFromString(std::string inString);
+	static const char* themeToString(Theme theme);
+
+	static WindowIcon iconFromString(std::string inString);
+	static const char* iconToString(WindowIcon icon);
+};
diff --git a/include/panda_qt/config_window.hpp b/include/panda_qt/config_window.hpp
index 4a523879..3cf4a1c8 100644
--- a/include/panda_qt/config_window.hpp
+++ b/include/panda_qt/config_window.hpp
@@ -1,30 +1,56 @@
 #pragma once
 
 #include <QApplication>
+#include <QCheckBox>
 #include <QComboBox>
 #include <QDialog>
+#include <QListWidget>
 #include <QPalette>
+#include <QStackedWidget>
+#include <QTextEdit>
 #include <QWidget>
 #include <QtWidgets>
+#include <array>
+#include <functional>
+#include <utility>
+
+#include "emulator.hpp"
+#include "frontend_settings.hpp"
 
 class ConfigWindow : public QDialog {
 	Q_OBJECT
 
   private:
-	enum class Theme : int {
-		System = 0,
-		Light = 1,
-		Dark = 2,
-		GreetingsCat = 3,
-		Cream = 4,
-	};
+	using ConfigCallback = std::function<void()>;
+	using IconCallback = std::function<void(const QString&)>;
 
-	Theme currentTheme;
-	QComboBox* themeSelect = nullptr;
+	using Theme = FrontendSettings::Theme;
+	using WindowIcon = FrontendSettings::WindowIcon;
 
-	void setTheme(Theme theme);
+	QTextEdit* helpText = nullptr;
+	QListWidget* widgetList = nullptr;
+	QStackedWidget* widgetContainer = nullptr;
+
+	static constexpr size_t settingWidgetCount = 6;
+	std::array<QString, settingWidgetCount> helpTexts;
+
+	// The config class holds a copy of the emulator config which it edits and sends
+	// over to the emulator in a thread-safe manner
+	EmulatorConfig config;
+
+	ConfigCallback updateConfig;
+	IconCallback updateIcon;
+
+	void addWidget(QWidget* widget, QString title, QString icon, QString helpText);
+	void setTheme(FrontendSettings::Theme theme);
+	void setIcon(FrontendSettings::WindowIcon icon);
 
   public:
-	ConfigWindow(QWidget* parent = nullptr);
+	ConfigWindow(ConfigCallback configCallback, IconCallback iconCallback, const EmulatorConfig& config, QWidget* parent = nullptr);
 	~ConfigWindow();
+
+	EmulatorConfig& getConfig() { return config; }
+
+  private:
+	Emulator* emu;
 };
diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp
index eb6b30e0..eb1cfb16 100644
--- a/include/panda_qt/main_window.hpp
+++ b/include/panda_qt/main_window.hpp
@@ -51,6 +51,7 @@ class MainWindow : public QMainWindow {
 		ReleaseTouchscreen,
 		ReloadUbershader,
 		SetScreenSize,
+		UpdateConfig,
 	};
 
 	// Tagged union representing our message queue messages
diff --git a/include/renderdoc.hpp b/include/renderdoc.hpp
index 02f2ade4..9c7de1e3 100644
--- a/include/renderdoc.hpp
+++ b/include/renderdoc.hpp
@@ -23,6 +23,9 @@ namespace Renderdoc {
 	// Sets output directory for captures
 	void setOutputDir(const std::string& path, const std::string& prefix);
 
+	// Returns whether Renderdoc has been loaded
+	bool isLoaded();
+
 	// Returns whether we've compiled with Renderdoc support
 	static constexpr bool isSupported() { return true; }
 }  // namespace Renderdoc
@@ -34,6 +37,7 @@ namespace Renderdoc {
 	static void triggerCapture() { Helpers::panic("Tried to trigger a Renderdoc capture while support for renderdoc is disabled"); }
 	static void setOutputDir(const std::string& path, const std::string& prefix) {}
 	static constexpr bool isSupported() { return false; }
+	static constexpr bool isLoaded() { return false; }
 }  // namespace Renderdoc
 #endif
 
diff --git a/src/config.cpp b/src/config.cpp
index 93aed106..a8c88a68 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -130,6 +130,16 @@ void EmulatorConfig::load() {
 			sdWriteProtected = toml::find_or<toml::boolean>(sd, "WriteProtectVirtualSD", false);
 		}
 	}
+
+	if (data.contains("UI")) {
+		auto uiResult = toml::expect<toml::value>(data.at("UI"));
+		if (uiResult.is_ok()) {
+			auto ui = uiResult.unwrap();
+
+			frontendSettings.theme = FrontendSettings::themeFromString(toml::find_or<std::string>(ui, "Theme", "dark"));
+			frontendSettings.icon = FrontendSettings::iconFromString(toml::find_or<std::string>(ui, "WindowIcon", "rpog"));
+		}
+	}
 }
 
 void EmulatorConfig::save() {
@@ -186,6 +196,9 @@ void EmulatorConfig::save() {
 	data["SD"]["UseVirtualSD"] = sdCardInserted;
 	data["SD"]["WriteProtectVirtualSD"] = sdWriteProtected;
 
+	data["UI"]["Theme"] = std::string(FrontendSettings::themeToString(frontendSettings.theme));
+	data["UI"]["WindowIcon"] = std::string(FrontendSettings::iconToString(frontendSettings.icon));
+
 	std::ofstream file(path, std::ios::out);
 	file << data;
 	file.close();
diff --git a/src/emulator.cpp b/src/emulator.cpp
index fc25eacb..1bb117b5 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -444,3 +444,24 @@ void Emulator::loadRenderdoc() {
 	Renderdoc::loadRenderdoc();
 	Renderdoc::setOutputDir(capturePath, "");
 }
+
+void Emulator::reloadSettings() {
+	setAudioEnabled(config.audioEnabled);
+
+	if (Renderdoc::isSupported() && config.enableRenderdoc && !Renderdoc::isLoaded()) {
+		loadRenderdoc();
+	}
+
+#ifdef PANDA3DS_ENABLE_DISCORD_RPC
+	// Reload RPC setting if we're compiling with RPC support
+
+	if (discordRpc.running() != config.discordRpcEnabled) {
+		if (config.discordRpcEnabled) {
+			discordRpc.init();
+			updateDiscord();
+		} else {
+			discordRpc.stop();
+		}
+	}
+#endif
+}
\ No newline at end of file
diff --git a/src/frontend_settings.cpp b/src/frontend_settings.cpp
new file mode 100644
index 00000000..16bae361
--- /dev/null
+++ b/src/frontend_settings.cpp
@@ -0,0 +1,64 @@
+#include "frontend_settings.hpp"
+
+#include <algorithm>
+#include <cctype>
+#include <unordered_map>
+
+// Frontend setting serialization/deserialization functions
+
+FrontendSettings::Theme FrontendSettings::themeFromString(std::string inString) {
+	// Transform to lower-case to make the setting case-insensitive
+	std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); });
+
+	static const std::unordered_map<std::string, Theme> map = {
+		{"system", Theme::System}, {"light", Theme::Light}, {"dark", Theme::Dark}, {"greetingscat", Theme::GreetingsCat}, {"cream", Theme::Cream},
+	};
+
+	if (auto search = map.find(inString); search != map.end()) {
+		return search->second;
+	}
+
+	// Default to dark theme
+	return Theme::Dark;
+}
+
+const char* FrontendSettings::themeToString(Theme theme) {
+	switch (theme) {
+		case Theme::System: return "system";
+		case Theme::Light: return "light";
+		case Theme::GreetingsCat: return "greetingscat";
+		case Theme::Cream: return "cream";
+
+		case Theme::Dark:
+		default: return "dark";
+	}
+}
+
+FrontendSettings::WindowIcon FrontendSettings::iconFromString(std::string inString) {  // Transform to lower-case to make the setting case-insensitive
+	std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); });
+
+	static const std::unordered_map<std::string, WindowIcon> map = {
+		{"rpog", WindowIcon::Rpog},
+		{"rsyn", WindowIcon::Rsyn},
+		{"rcow", WindowIcon::Rcow},
+		{"rnap", WindowIcon::Rnap},
+	};
+
+	if (auto search = map.find(inString); search != map.end()) {
+		return search->second;
+	}
+
+	// Default to the icon rpog icon
+	return WindowIcon::Rpog;
+}
+
+const char* FrontendSettings::iconToString(WindowIcon icon) {
+	switch (icon) {
+		case WindowIcon::Rsyn: return "rsyn";
+		case WindowIcon::Rcow: return "rcow";
+		case WindowIcon::Rnap: return "rnap";
+
+		case WindowIcon::Rpog:
+		default: return "rpog";
+	}
+}
\ No newline at end of file
diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp
index 75293742..64cade49 100644
--- a/src/panda_qt/config_window.cpp
+++ b/src/panda_qt/config_window.cpp
@@ -1,26 +1,286 @@
 #include "panda_qt/config_window.hpp"
 
-ConfigWindow::ConfigWindow(QWidget* parent) : QDialog(parent) {
+ConfigWindow::ConfigWindow(ConfigCallback configCallback, IconCallback iconCallback, const EmulatorConfig& emuConfig, QWidget* parent)
+	: QDialog(parent), config(emuConfig) {
 	setWindowTitle(tr("Configuration"));
 
+	updateConfig = std::move(configCallback);
+	updateIcon = std::move(iconCallback);
+
 	// Set up theme selection
-	setTheme(Theme::Dark);
-	themeSelect = new QComboBox(this);
+	setTheme(config.frontendSettings.theme);
+	setIcon(config.frontendSettings.icon);
+
+	// Initialize the widget list and the widget container widgets
+	widgetList = new QListWidget(this);
+	widgetContainer = new QStackedWidget(this);
+
+	helpText = new QTextEdit(this);
+	helpText->setReadOnly(true);
+
+	helpText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+	helpText->setFixedHeight(50);
+
+	widgetList->setMinimumWidth(100);
+	widgetList->setMaximumWidth(100);
+	widgetList->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+	widgetList->setPalette(QPalette(QColor(25, 25, 25)));
+
+	widgetList->setCurrentRow(0);
+	widgetContainer->setCurrentIndex(0);
+
+	connect(widgetList, &QListWidget::currentRowChanged, this, [&](int row) {
+		widgetContainer->setCurrentIndex(row);
+		helpText->setText(helpTexts[row]);
+	});
+
+	auto connectCheckbox = [&](QCheckBox* checkbox, bool& setting) {
+		checkbox->setChecked(setting);
+
+		connect(checkbox, &QCheckBox::toggled, this, [&](bool checked) {
+			setting = checked;
+			updateConfig();
+		});
+	};
+
+	QVBoxLayout* mainLayout = new QVBoxLayout();
+	QHBoxLayout* hLayout = new QHBoxLayout();
+
+	// Set up widget layouts
+	setLayout(mainLayout);
+	mainLayout->addLayout(hLayout);
+	mainLayout->addWidget(helpText);
+
+	hLayout->setAlignment(Qt::AlignLeft);
+	hLayout->addWidget(widgetList);
+	hLayout->addWidget(widgetContainer);
+
+	// Interface settings
+	QGroupBox* guiGroupBox = new QGroupBox(tr("Interface Settings"), this);
+	QFormLayout* guiLayout = new QFormLayout(guiGroupBox);
+	guiLayout->setHorizontalSpacing(20);
+	guiLayout->setVerticalSpacing(10);
+
+	QComboBox* themeSelect = new QComboBox();
 	themeSelect->addItem(tr("System"));
 	themeSelect->addItem(tr("Light"));
 	themeSelect->addItem(tr("Dark"));
 	themeSelect->addItem(tr("Greetings Cat"));
 	themeSelect->addItem(tr("Cream"));
-	themeSelect->setCurrentIndex(static_cast<int>(currentTheme));
+	themeSelect->setCurrentIndex(static_cast<int>(config.frontendSettings.theme));
+	connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) {
+		config.frontendSettings.theme = static_cast<Theme>(index);
+		setTheme(static_cast<Theme>(index));
 
-	themeSelect->setGeometry(40, 40, 100, 50);
-	themeSelect->show();
-	connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast<Theme>(index)); });
+		updateConfig();
+	});
+	guiLayout->addRow(tr("Color theme"), themeSelect);
+
+	QComboBox* iconSelect = new QComboBox();
+	iconSelect->addItem(tr("Happy panda"));
+	iconSelect->addItem(tr("Happy panda (colourful)"));
+	iconSelect->addItem(tr("Sleepy panda"));
+	iconSelect->addItem(tr("Cow panda"));
+	iconSelect->setCurrentIndex(static_cast<int>(config.frontendSettings.icon));
+
+	connect(iconSelect, &QComboBox::currentIndexChanged, this, [&](int index) {
+		config.frontendSettings.icon = static_cast<WindowIcon>(index);
+		setIcon(static_cast<WindowIcon>(index));
+
+		updateConfig();
+	});
+	guiLayout->addRow(tr("Window icon"), iconSelect);
+
+	QCheckBox* showAppVersion = new QCheckBox(tr("Show version on window title"));
+	connectCheckbox(showAppVersion, config.windowSettings.showAppVersion);
+	guiLayout->addRow(showAppVersion);
+
+	QCheckBox* rememberPosition = new QCheckBox(tr("Remember window position"));
+	connectCheckbox(rememberPosition, config.windowSettings.rememberPosition);
+	guiLayout->addRow(rememberPosition);
+
+	// General settings
+	QGroupBox* genGroupBox = new QGroupBox(tr("General Settings"), this);
+	QFormLayout* genLayout = new QFormLayout(genGroupBox);
+	genLayout->setHorizontalSpacing(20);
+	genLayout->setVerticalSpacing(10);
+
+	QLineEdit* defaultRomPath = new QLineEdit;
+	defaultRomPath->setText(QString::fromStdU16String(config.defaultRomPath.u16string()));
+	connect(defaultRomPath, &QLineEdit::textChanged, this, [&](const QString& text) {
+		config.defaultRomPath = text.toStdString();
+		updateConfig();
+	});
+	QPushButton* browseRomPath = new QPushButton(tr("Browse..."));
+	browseRomPath->setAutoDefault(false);
+	connect(browseRomPath, &QPushButton::pressed, this, [&, defaultRomPath]() {
+		QString newPath = QFileDialog::getExistingDirectory(
+			this, tr("Select Directory"), QString::fromStdU16String(config.defaultRomPath.u16string()),
+			QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
+		);
+		if (!newPath.isEmpty()) {
+			defaultRomPath->setText(newPath);
+		}
+	});
+	QHBoxLayout* romLayout = new QHBoxLayout;
+	romLayout->setSpacing(4);
+	romLayout->addWidget(defaultRomPath);
+	romLayout->addWidget(browseRomPath);
+	genLayout->addRow(tr("Default ROMs path"), romLayout);
+
+	QCheckBox* discordRpcEnabled = new QCheckBox(tr("Enable Discord RPC"));
+	connectCheckbox(discordRpcEnabled, config.discordRpcEnabled);
+	genLayout->addRow(discordRpcEnabled);
+
+	QCheckBox* usePortableBuild = new QCheckBox(tr("Use portable build"));
+	connectCheckbox(usePortableBuild, config.usePortableBuild);
+	genLayout->addRow(usePortableBuild);
+
+	QCheckBox* printAppVersion = new QCheckBox(tr("Print version in console output"));
+	connectCheckbox(printAppVersion, config.printAppVersion);
+	genLayout->addRow(printAppVersion);
+
+	// Graphics settings
+	QGroupBox* gpuGroupBox = new QGroupBox(tr("Graphics Settings"), this);
+	QFormLayout* gpuLayout = new QFormLayout(gpuGroupBox);
+	gpuLayout->setHorizontalSpacing(20);
+	gpuLayout->setVerticalSpacing(10);
+
+	QComboBox* rendererType = new QComboBox;
+	rendererType->addItem(tr("Null"));
+	rendererType->addItem(tr("OpenGL"));
+	rendererType->addItem(tr("Vulkan"));
+	rendererType->setCurrentIndex(static_cast<int>(config.rendererType));
+	connect(rendererType, &QComboBox::currentIndexChanged, this, [&](int index) {
+		config.rendererType = static_cast<RendererType>(index);
+		updateConfig();
+	});
+	gpuLayout->addRow(tr("GPU renderer"), rendererType);
+
+	QCheckBox* enableRenderdoc = new QCheckBox(tr("Enable Renderdoc"));
+	connectCheckbox(enableRenderdoc, config.enableRenderdoc);
+	gpuLayout->addRow(enableRenderdoc);
+
+	QCheckBox* shaderJitEnabled = new QCheckBox(tr("Enable shader JIT"));
+	connectCheckbox(shaderJitEnabled, config.shaderJitEnabled);
+	gpuLayout->addRow(shaderJitEnabled);
+
+	QCheckBox* vsyncEnabled = new QCheckBox(tr("Enable VSync"));
+	connectCheckbox(vsyncEnabled, config.vsyncEnabled);
+	gpuLayout->addRow(vsyncEnabled);
+
+	QCheckBox* useUbershaders = new QCheckBox(tr("Use ubershaders (No stutter, maybe slower)"));
+	connectCheckbox(useUbershaders, config.useUbershaders);
+	gpuLayout->addRow(useUbershaders);
+
+	QCheckBox* accurateShaderMul = new QCheckBox(tr("Accurate shader multiplication"));
+	connectCheckbox(accurateShaderMul, config.accurateShaderMul);
+	gpuLayout->addRow(accurateShaderMul);
+
+	QCheckBox* accelerateShaders = new QCheckBox(tr("Accelerate shaders"));
+	connectCheckbox(accelerateShaders, config.accelerateShaders);
+	gpuLayout->addRow(accelerateShaders);
+
+	QCheckBox* forceShadergenForLights = new QCheckBox(tr("Force shadergen when rendering lights"));
+	connectCheckbox(forceShadergenForLights, config.forceShadergenForLights);
+	gpuLayout->addRow(forceShadergenForLights);
+
+	QSpinBox* lightShadergenThreshold = new QSpinBox;
+	lightShadergenThreshold->setRange(1, 8);
+	lightShadergenThreshold->setValue(config.lightShadergenThreshold);
+	connect(lightShadergenThreshold, &QSpinBox::valueChanged, this, [&](int value) {
+		config.lightShadergenThreshold = static_cast<int>(value);
+		updateConfig();
+	});
+	gpuLayout->addRow(tr("Light threshold for forcing shadergen"), lightShadergenThreshold);
+
+	// Audio settings
+	QGroupBox* spuGroupBox = new QGroupBox(tr("Audio Settings"), this);
+	QFormLayout* audioLayout = new QFormLayout(spuGroupBox);
+	audioLayout->setHorizontalSpacing(20);
+	audioLayout->setVerticalSpacing(10);
+
+	QComboBox* dspType = new QComboBox;
+	dspType->addItem(tr("Null"));
+	dspType->addItem(tr("LLE"));
+	dspType->addItem(tr("HLE"));
+	dspType->setCurrentIndex(static_cast<int>(config.dspType));
+	connect(dspType, &QComboBox::currentIndexChanged, this, [&](int index) {
+		config.dspType = static_cast<Audio::DSPCore::Type>(index);
+		updateConfig();
+	});
+	audioLayout->addRow(tr("DSP emulation"), dspType);
+
+	QCheckBox* audioEnabled = new QCheckBox(tr("Enable audio"));
+	connectCheckbox(audioEnabled, config.audioEnabled);
+	audioLayout->addRow(audioEnabled);
+
+	QCheckBox* aacEnabled = new QCheckBox(tr("Enable AAC audio"));
+	connectCheckbox(aacEnabled, config.aacEnabled);
+	audioLayout->addRow(aacEnabled);
+
+	QCheckBox* printDSPFirmware = new QCheckBox(tr("Print DSP firmware"));
+	connectCheckbox(printDSPFirmware, config.printDSPFirmware);
+	audioLayout->addRow(printDSPFirmware);
+
+	QCheckBox* muteAudio = new QCheckBox(tr("Mute audio device"));
+	connectCheckbox(muteAudio, config.audioDeviceConfig.muteAudio);
+	audioLayout->addRow(muteAudio);
+
+	QSpinBox* volumeRaw = new QSpinBox();
+	volumeRaw->setRange(0, 200);
+	volumeRaw->setValue(config.audioDeviceConfig.volumeRaw*  100);
+	connect(volumeRaw, &QSpinBox::valueChanged, this, [&](int value) {
+		config.audioDeviceConfig.volumeRaw = static_cast<float>(value) / 100.0f;
+		updateConfig();
+	});
+	audioLayout->addRow(tr("Audio device volume"), volumeRaw);
+
+	// Battery settings
+	QGroupBox* batGroupBox = new QGroupBox(tr("Battery Settings"), this);
+	QFormLayout* batLayout = new QFormLayout(batGroupBox);
+	batLayout->setHorizontalSpacing(20);
+	batLayout->setVerticalSpacing(10);
+
+	QSpinBox* batteryPercentage = new QSpinBox;
+	batteryPercentage->setRange(1, 100);
+	batteryPercentage->setValue(config.batteryPercentage);
+	connect(batteryPercentage, &QSpinBox::valueChanged, this, [&](int value) {
+		config.batteryPercentage = static_cast<int>(value);
+		updateConfig();
+	});
+	batLayout->addRow(tr("Battery percentage"), batteryPercentage);
+
+	QCheckBox* chargerPlugged = new QCheckBox(tr("Charger plugged"));
+	connectCheckbox(chargerPlugged, config.chargerPlugged);
+	batLayout->addRow(chargerPlugged);
+
+	// SD Card settings
+	QGroupBox* sdcGroupBox = new QGroupBox(tr("SD Card Settings"), this);
+	QFormLayout* sdcLayout = new QFormLayout(sdcGroupBox);
+	sdcLayout->setHorizontalSpacing(20);
+	sdcLayout->setVerticalSpacing(10);
+
+	QCheckBox* sdCardInserted = new QCheckBox(tr("Enable virtual SD card"));
+	connectCheckbox(sdCardInserted, config.sdCardInserted);
+	sdcLayout->addRow(sdCardInserted);
+
+	QCheckBox* sdWriteProtected = new QCheckBox(tr("Write protect virtual SD card"));
+	connectCheckbox(sdWriteProtected, config.sdWriteProtected);
+	sdcLayout->addRow(sdWriteProtected);
+
+	// Add all our settings widgets to our widget list
+	addWidget(guiGroupBox, tr("Interface"), ":/docs/img/sparkling_icon.png", tr("User Interface settings"));
+	addWidget(genGroupBox, tr("General"), ":/docs/img/settings_icon.png", tr("General emulator settings"));
+	addWidget(gpuGroupBox, tr("Graphics"), ":/docs/img/display_icon.png", tr("Graphics emulation and output settings"));
+	addWidget(spuGroupBox, tr("Audio"), ":/docs/img/speaker_icon.png", tr("Audio emulation and output settings"));
+	addWidget(batGroupBox, tr("Battery"), ":/docs/img/battery_icon.png", tr("Battery emulation settings"));
+	addWidget(sdcGroupBox, tr("SD Card"), ":/docs/img/sdcard_icon.png", tr("SD Card emulation settings"));
+
+	widgetList->setCurrentRow(0);
 }
 
 void ConfigWindow::setTheme(Theme theme) {
-	currentTheme = theme;
-
 	switch (theme) {
 		case Theme::Dark: {
 			QApplication::setStyle(QStyleFactory::create("Fusion"));
@@ -119,4 +379,36 @@ void ConfigWindow::setTheme(Theme theme) {
 	}
 }
 
-ConfigWindow::~ConfigWindow() { delete themeSelect; }
+void ConfigWindow::setIcon(WindowIcon icon) {
+	switch (icon) {
+		case WindowIcon::Rsyn: updateIcon(":/docs/img/rsyn_icon.png"); break;
+		case WindowIcon::Rnap: updateIcon(":/docs/img/rnap_icon.png"); break;
+		case WindowIcon::Rcow: updateIcon(":/docs/img/rcow_icon.png"); break;
+
+		case WindowIcon::Rpog:
+		default: updateIcon(":/docs/img/rpog_icon.png"); break;
+	}
+}
+
+void ConfigWindow::addWidget(QWidget* widget, QString title, QString icon, QString helpText) {
+	const int index = widgetList->count();
+
+	QListWidgetItem* item = new QListWidgetItem(widgetList);
+	item->setText(title);
+	if (!icon.isEmpty()) {
+		item->setIcon(QIcon::fromTheme(icon));
+	}
+
+	widgetContainer->addWidget(widget);
+
+	if (index >= settingWidgetCount) {
+		Helpers::panic("Qt: ConfigWindow::settingWidgetCount has not been updated correctly!");
+	}
+	helpTexts[index] = std::move(helpText);
+}
+
+ConfigWindow::~ConfigWindow() {
+	delete helpText;
+	delete widgetList;
+	delete widgetContainer;
+}
diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp
index 93ce2613..fa3efae7 100644
--- a/src/panda_qt/main_window.cpp
+++ b/src/panda_qt/main_window.cpp
@@ -15,7 +15,6 @@
 
 MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) {
 	setWindowTitle("Alber");
-	setWindowIcon(QIcon(":/docs/img/rpog_icon.png"));
 
 	// Enable drop events for loading ROMs
 	setAcceptDrops(true);
@@ -81,7 +80,6 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
 
 	// Set up misc objects
 	aboutWindow = new AboutWindow(nullptr);
-	configWindow = new ConfigWindow(this);
 	cheatsEditor = new CheatsWindow(emu, {}, this);
 	patchWindow = new PatchWindow(this);
 	luaEditor = new TextEditorWindow(this, "script.lua", "");
@@ -92,6 +90,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
 		shaderEditor->setText(emu->getRenderer()->getUbershader());
 	}
 
+	configWindow = new ConfigWindow(
+		[&]() {
+			EmulatorMessage message{.type = MessageType::UpdateConfig};
+			sendMessage(message);
+		},
+		[&](const QString& icon) { setWindowIcon(QIcon(icon)); }, emu->getConfig(), this
+	);
+
 	auto args = QCoreApplication::arguments();
 	if (args.size() > 1) {
 		auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String();
@@ -410,6 +416,14 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) {
 			screen->resizeSurface(width, height);
 			break;
 		}
+
+		case MessageType::UpdateConfig:
+			emu->getConfig() = configWindow->getConfig();
+			emu->reloadSettings();
+
+			// Save new settings to disk
+			emu->getConfig().save();
+			break;
 	}
 }
 
diff --git a/src/renderdoc.cpp b/src/renderdoc.cpp
index 1de9c451..43627b66 100644
--- a/src/renderdoc.cpp
+++ b/src/renderdoc.cpp
@@ -23,6 +23,8 @@ namespace Renderdoc {
 	};
 
 	static CaptureState captureState{CaptureState::Idle};
+	static bool renderdocLoaded{false};
+
 	RENDERDOC_API_1_6_0* rdocAPI{};
 
 	void loadRenderdoc() {
@@ -73,6 +75,8 @@ namespace Renderdoc {
 		}
 #endif
 		if (rdocAPI) {
+			renderdocLoaded = true;
+
 			// Disable default capture keys as they suppose to trigger present-to-present capturing
 			// and it is not what we want
 			rdocAPI->SetCaptureKeys(nullptr, 0);
@@ -115,5 +119,7 @@ namespace Renderdoc {
 			rdocAPI->SetCaptureFilePathTemplate((path + '\\' + prefix).c_str());
 		}
 	}
+
+	bool isLoaded() { return renderdocLoaded; }
 }  // namespace Renderdoc
 #endif
\ No newline at end of file