diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index b7ae56f9b4..40e75acff7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -72,7 +72,14 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
USE_LRU_CACHE("use_lru_cache"),
DONT_SHOW_DRIVER_SHADER_WARNING("dont_show_driver_shader_warning"),
- ENABLE_OVERLAY("enable_overlay");
+ ENABLE_OVERLAY("enable_overlay"),
+
+ // GPU Logging
+ GPU_LOGGING_ENABLED("gpu_logging_enabled"),
+ GPU_LOG_VULKAN_CALLS("gpu_log_vulkan_calls"),
+ GPU_LOG_SHADER_DUMPS("gpu_log_shader_dumps"),
+ GPU_LOG_MEMORY_TRACKING("gpu_log_memory_tracking"),
+ GPU_LOG_DRIVER_DEBUG("gpu_log_driver_debug");
// external fun isFrameSkippingEnabled(): Boolean
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
index 245fe9c9ca..aa4a939952 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -9,7 +9,8 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting(override val key: String) : AbstractByteSetting {
- AUDIO_VOLUME("volume"),;
+ AUDIO_VOLUME("volume"),
+ GPU_LOG_LEVEL("gpu_log_level");
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index 96db3cf7c0..be3b2f4a48 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -67,7 +67,8 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MY_PAGE_APPLET("my_page_applet_mode"),
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
OVERLAY_GRID_SIZE("overlay_grid_size"),
- DEBUG_KNOBS("debug_knobs")
+ DEBUG_KNOBS("debug_knobs"),
+ GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size")
;
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 8396672ba3..afa76362ff 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -836,6 +836,62 @@ abstract class SettingsItem(
)
)
+ // GPU Logging settings
+ put(
+ SwitchSetting(
+ BooleanSetting.GPU_LOGGING_ENABLED,
+ titleId = R.string.gpu_logging_enabled,
+ descriptionId = R.string.gpu_logging_enabled_description
+ )
+ )
+ put(
+ SingleChoiceSetting(
+ ByteSetting.GPU_LOG_LEVEL,
+ titleId = R.string.gpu_log_level,
+ descriptionId = R.string.gpu_log_level_description,
+ choicesId = R.array.gpuLogLevelEntries,
+ valuesId = R.array.gpuLogLevelValues
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.GPU_LOG_VULKAN_CALLS,
+ titleId = R.string.gpu_log_vulkan_calls,
+ descriptionId = R.string.gpu_log_vulkan_calls_description
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.GPU_LOG_SHADER_DUMPS,
+ titleId = R.string.gpu_log_shader_dumps,
+ descriptionId = R.string.gpu_log_shader_dumps_description
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.GPU_LOG_MEMORY_TRACKING,
+ titleId = R.string.gpu_log_memory_tracking,
+ descriptionId = R.string.gpu_log_memory_tracking_description
+ )
+ )
+ put(
+ SwitchSetting(
+ BooleanSetting.GPU_LOG_DRIVER_DEBUG,
+ titleId = R.string.gpu_log_driver_debug,
+ descriptionId = R.string.gpu_log_driver_debug_description
+ )
+ )
+ put(
+ SpinBoxSetting(
+ IntSetting.GPU_LOG_RING_BUFFER_SIZE,
+ titleId = R.string.gpu_log_ring_buffer_size,
+ descriptionId = R.string.gpu_log_ring_buffer_size_description,
+ valueHint = R.string.gpu_log_ring_buffer_size_hint,
+ min = 64,
+ max = 4096
+ )
+ )
+
val fastmem = object : AbstractBooleanSetting {
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.FASTMEM.getBoolean() &&
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 27222c85af..b6cb7acf1e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -1220,6 +1220,15 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.general))
add(IntSetting.DEBUG_KNOBS.key)
+
+ add(HeaderSetting(R.string.gpu_logging_header))
+ add(BooleanSetting.GPU_LOGGING_ENABLED.key)
+ add(ByteSetting.GPU_LOG_LEVEL.key)
+ add(BooleanSetting.GPU_LOG_VULKAN_CALLS.key)
+ add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
+ add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
+ add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
+ add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 6cb35014b4..464572e777 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -222,6 +222,14 @@ class HomeSettingsFragment : Fragment() {
{ shareLog() }
)
)
+ add(
+ HomeSetting(
+ R.string.share_gpu_log,
+ R.string.share_gpu_log_description,
+ R.drawable.ic_log,
+ { shareGpuLog() }
+ )
+ )
add(
HomeSetting(
R.string.open_user_folder,
@@ -408,6 +416,40 @@ class HomeSettingsFragment : Fragment() {
}
}
+ private fun shareGpuLog() {
+ val currentLog = DocumentFile.fromSingleUri(
+ mainActivity,
+ DocumentsContract.buildDocumentUri(
+ DocumentProvider.AUTHORITY,
+ "${DocumentProvider.ROOT_ID}/log/eden_gpu.log"
+ )
+ )!!
+ val oldLog = DocumentFile.fromSingleUri(
+ mainActivity,
+ DocumentsContract.buildDocumentUri(
+ DocumentProvider.AUTHORITY,
+ "${DocumentProvider.ROOT_ID}/log/eden_gpu.log.old.txt"
+ )
+ )!!
+
+ val intent = Intent(Intent.ACTION_SEND)
+ .setDataAndType(currentLog.uri, FileUtil.TEXT_PLAIN)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ if (!Log.gameLaunched && oldLog.exists()) {
+ intent.putExtra(Intent.EXTRA_STREAM, oldLog.uri)
+ startActivity(Intent.createChooser(intent, getText(R.string.share_gpu_log)))
+ } else if (currentLog.exists()) {
+ intent.putExtra(Intent.EXTRA_STREAM, currentLog.uri)
+ startActivity(Intent.createChooser(intent, getText(R.string.share_gpu_log)))
+ } else {
+ Toast.makeText(
+ requireContext(),
+ getText(R.string.share_gpu_log_missing),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 3e77d82267..69f1590844 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -631,4 +631,21 @@
- @string/error_keys_invalid_filename
- @string/error_keys_failed_init
+
+
+
+ - Off
+ - Errors Only
+ - Standard
+ - Verbose
+ - All
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index ec4f9a2cf4..3740a54639 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -325,6 +325,9 @@
Share debug logs
Share Eden\'s log file to debug issues
No log file found
+ Share GPU logs
+ Share Eden\'s GPU log file to debug graphics issues
+ No GPU log file found
Install game content
Install game updates or DLC
Installing content…
@@ -552,6 +555,26 @@
Flush debug logs by line
Flushes debugging logs on each line written, making debugging easier in cases of crashing or freezing.
+
+ GPU Logging
+ GPU Logging
+ Enable GPU Logging
+ Log GPU operations to eden_gpu.log for debugging Adreno drivers
+ Log Level
+ Detail level for GPU logs (higher = more detail, more overhead)
+ Logging Features
+ Log Vulkan API Calls
+ Track all Vulkan API calls in ring buffer
+ Dump Shaders
+ Save compiled shader SPIR-V to files
+ Track GPU Memory
+ Monitor GPU memory allocations and deallocations
+ Driver Debug Info
+ Capture driver-specific debug information (Turnip breadcrumbs, etc.)
+ Ring Buffer Size
+ Number of recent Vulkan calls to track (default: 512)
+ 64 to 4096 entries
+
General
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 2c88356888..1c28adafe5 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -49,6 +49,7 @@ SWITCHABLE(CpuBackend, true);
SWITCHABLE(CpuAccuracy, true);
SWITCHABLE(FullscreenMode, true);
SWITCHABLE(GpuAccuracy, true);
+SWITCHABLE(GpuLogLevel, true);
SWITCHABLE(Language, true);
SWITCHABLE(MemoryLayout, true);
SWITCHABLE(NvdecEmulation, false);
diff --git a/src/common/settings.h b/src/common/settings.h
index d5bf9f7660..09c05a812a 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -738,6 +738,18 @@ struct Values {
Setting perform_vulkan_check{linkage, true, "perform_vulkan_check", Category::Debugging};
Setting disable_web_applet{linkage, true, "disable_web_applet", Category::Debugging};
+ // GPU Logging
+ Setting gpu_logging_enabled{linkage, true, "gpu_logging_enabled", Category::Debugging};
+ SwitchableSetting gpu_log_level{linkage, GpuLogLevel::Standard, "gpu_log_level",
+ Category::Debugging};
+ Setting gpu_log_vulkan_calls{linkage, true, "gpu_log_vulkan_calls", Category::Debugging};
+ Setting gpu_log_shader_dumps{linkage, false, "gpu_log_shader_dumps", Category::Debugging};
+ Setting gpu_log_memory_tracking{linkage, true, "gpu_log_memory_tracking",
+ Category::Debugging};
+ Setting gpu_log_driver_debug{linkage, true, "gpu_log_driver_debug", Category::Debugging};
+ Setting gpu_log_ring_buffer_size{linkage, 512, "gpu_log_ring_buffer_size",
+ Category::Debugging};
+
SwitchableSetting debug_knobs{linkage,
0,
0,
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index 7d8d3f40d4..30d075565b 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Torzu Emulator Project
@@ -154,6 +154,7 @@ ENUM(GpuUnswizzle, VeryLow, Low, Normal, Medium, High)
ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
+ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
template
inline std::string_view CanonicalizeEnum(Type id) {
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index c94b66e6bc..ed77ae8934 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -1,10 +1,11 @@
-# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(host_shaders)
+add_subdirectory(gpu_logging)
if(LIBVA_FOUND)
set_source_files_properties(host1x/ffmpeg/ffmpeg.cpp
@@ -313,7 +314,7 @@ add_library(video_core STATIC
)
target_link_libraries(video_core PUBLIC common core)
-target_link_libraries(video_core PUBLIC glad shader_recompiler stb bc_decoder)
+target_link_libraries(video_core PUBLIC glad shader_recompiler stb bc_decoder gpu_logging)
if (YUZU_USE_EXTERNAL_FFMPEG)
add_dependencies(video_core ffmpeg-build)
diff --git a/src/video_core/gpu_logging/CMakeLists.txt b/src/video_core/gpu_logging/CMakeLists.txt
new file mode 100644
index 0000000000..9a9f1e5f8d
--- /dev/null
+++ b/src/video_core/gpu_logging/CMakeLists.txt
@@ -0,0 +1,25 @@
+# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+add_library(gpu_logging STATIC
+ gpu_logging.cpp
+ gpu_logging.h
+ gpu_state_capture.cpp
+ gpu_state_capture.h
+ qualcomm_debug.cpp
+ qualcomm_debug.h
+)
+
+if(ANDROID)
+ target_sources(gpu_logging PRIVATE
+ freedreno_debug.cpp
+ freedreno_debug.h
+ )
+endif()
+
+target_link_libraries(gpu_logging PUBLIC common)
+
+if(ANDROID)
+ # Link with adrenotools when available for future Qualcomm integration
+ # target_link_libraries(gpu_logging PUBLIC adrenotools)
+endif()
diff --git a/src/video_core/gpu_logging/freedreno_debug.cpp b/src/video_core/gpu_logging/freedreno_debug.cpp
new file mode 100644
index 0000000000..14ffe04f78
--- /dev/null
+++ b/src/video_core/gpu_logging/freedreno_debug.cpp
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifdef ANDROID
+
+#include "video_core/gpu_logging/freedreno_debug.h"
+#include "common/logging/log.h"
+
+#include
+
+namespace GPU::Logging::Freedreno {
+
+bool FreedrenoDebugger::is_initialized = false;
+
+void FreedrenoDebugger::Initialize() {
+ if (is_initialized) {
+ return;
+ }
+
+ is_initialized = true;
+ LOG_INFO(Render_Vulkan, "[Freedreno Debug] Initialized");
+}
+
+void FreedrenoDebugger::SetTUDebugFlags(const std::string& flags) {
+ if (flags.empty()) {
+ return;
+ }
+
+ // Set TU_DEBUG environment variable
+ // Note: This should be set BEFORE Vulkan driver is loaded
+ setenv("TU_DEBUG", flags.c_str(), 1);
+
+ LOG_INFO(Render_Vulkan, "[Freedreno Debug] TU_DEBUG set to: {}", flags);
+}
+
+void FreedrenoDebugger::EnableCommandStreamDump(bool frames_only) {
+ // Enable FD_RD_DUMP for command stream capture
+ const char* dump_flags = frames_only ? "frames" : "all";
+ setenv("FD_RD_DUMP", dump_flags, 1);
+
+ LOG_INFO(Render_Vulkan, "[Freedreno Debug] Command stream dump enabled: {}", dump_flags);
+}
+
+std::string FreedrenoDebugger::GetBreadcrumbs() {
+ // Breadcrumb reading requires driver-specific implementation
+ // This is a stub for future implementation
+ return "Breadcrumb capture not yet implemented";
+}
+
+} // namespace GPU::Logging::Freedreno
+
+#endif // ANDROID
diff --git a/src/video_core/gpu_logging/freedreno_debug.h b/src/video_core/gpu_logging/freedreno_debug.h
new file mode 100644
index 0000000000..feb57a0b2a
--- /dev/null
+++ b/src/video_core/gpu_logging/freedreno_debug.h
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#ifdef ANDROID
+
+#include
+
+namespace GPU::Logging::Freedreno {
+
+class FreedrenoDebugger {
+public:
+ // Initialize Freedreno debugging
+ static void Initialize();
+
+ // Set TU_DEBUG environment variable flags
+ static void SetTUDebugFlags(const std::string& flags);
+
+ // Enable command stream dump
+ static void EnableCommandStreamDump(bool frames_only = false);
+
+ // Get breadcrumb information (if available)
+ static std::string GetBreadcrumbs();
+
+private:
+ static bool is_initialized;
+};
+
+} // namespace GPU::Logging::Freedreno
+
+#endif // ANDROID
diff --git a/src/video_core/gpu_logging/gpu_logging.cpp b/src/video_core/gpu_logging/gpu_logging.cpp
new file mode 100644
index 0000000000..059e8a6c06
--- /dev/null
+++ b/src/video_core/gpu_logging/gpu_logging.cpp
@@ -0,0 +1,734 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "video_core/gpu_logging/gpu_logging.h"
+
+#include
+#include
+
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/literals.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+
+namespace GPU::Logging {
+
+// Static instance
+static GPULogger* g_instance = nullptr;
+
+GPULogger& GPULogger::GetInstance() {
+ if (!g_instance) {
+ g_instance = new GPULogger();
+ }
+ return *g_instance;
+}
+
+GPULogger::GPULogger() = default;
+
+GPULogger::~GPULogger() {
+ Shutdown();
+}
+
+void GPULogger::Initialize(LogLevel level, DriverType driver) {
+ if (initialized) {
+ LOG_WARNING(Render_Vulkan, "[GPU Logging] Already initialized");
+ return;
+ }
+
+ current_level = level;
+ detected_driver = driver;
+
+ if (current_level == LogLevel::Off) {
+ return;
+ }
+
+ // Create log directory
+ using namespace Common::FS;
+ const auto& log_dir = GetEdenPath(EdenPath::LogDir);
+ [[maybe_unused]] const bool log_dir_created = CreateDir(log_dir);
+
+ // Create GPU crashes directory
+ const auto crashes_dir = log_dir / "gpu_crashes";
+ [[maybe_unused]] const bool crashes_dir_created = CreateDir(crashes_dir);
+
+ // Open GPU log file
+ const auto gpu_log_path = log_dir / "eden_gpu.log";
+
+ // Rotate old log
+ const auto old_log_path = log_dir / "eden_gpu.log.old.txt";
+ RemoveFile(old_log_path);
+ [[maybe_unused]] const bool log_renamed = RenameFile(gpu_log_path, old_log_path);
+
+ // Open new log file
+ gpu_log_file = std::make_unique(
+ gpu_log_path, Common::FS::FileAccessMode::Write, Common::FS::FileType::TextFile);
+
+ if (!gpu_log_file->IsOpen()) {
+ LOG_ERROR(Render_Vulkan, "[GPU Logging] Failed to open GPU log file");
+ return;
+ }
+
+ // Initialize ring buffer
+ call_ring_buffer.resize(ring_buffer_size);
+
+ // Write header
+ const char* driver_name = "Unknown";
+ switch (detected_driver) {
+ case DriverType::Turnip:
+ driver_name = "Turnip (Mesa Freedreno)";
+ break;
+ case DriverType::Qualcomm:
+ driver_name = "Qualcomm Proprietary";
+ break;
+ default:
+ driver_name = "Unknown";
+ break;
+ }
+
+ const char* level_name = "Unknown";
+ switch (current_level) {
+ case LogLevel::Off:
+ level_name = "Off";
+ break;
+ case LogLevel::Errors:
+ level_name = "Errors";
+ break;
+ case LogLevel::Standard:
+ level_name = "Standard";
+ break;
+ case LogLevel::Verbose:
+ level_name = "Verbose";
+ break;
+ case LogLevel::All:
+ level_name = "All";
+ break;
+ }
+
+ const auto header = fmt::format(
+ "=== Eden GPU Logging Started ===\n"
+ "Timestamp: {}\n"
+ "Log Level: {}\n"
+ "Driver: {}\n"
+ "Ring Buffer Size: {}\n"
+ "================================\n\n",
+ FormatTimestamp(std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch())),
+ level_name, driver_name, ring_buffer_size);
+
+ WriteToLog(header);
+
+ // Note: Crash handler is initialized independently in EmulationSession::InitializeSystem()
+ // to ensure it remains active even if Vulkan device initialization fails
+
+ initialized = true;
+ LOG_INFO(Render_Vulkan, "[GPU Logging] Initialized with level: {}, driver: {}", level_name,
+ driver_name);
+}
+
+void GPULogger::Shutdown() {
+ if (!initialized) {
+ return;
+ }
+
+ // Write statistics
+ const auto stats = fmt::format(
+ "\n=== GPU Logging Statistics ===\n"
+ "Total Vulkan Calls: {}\n"
+ "Total Memory Allocations: {}\n"
+ "Total Memory Deallocations: {}\n"
+ "Peak Memory Usage: {}\n"
+ "Current Memory Usage: {}\n"
+ "Log Size: {} bytes\n"
+ "==============================\n",
+ total_vulkan_calls, total_allocations, total_deallocations,
+ FormatMemorySize(peak_allocated_bytes), FormatMemorySize(current_allocated_bytes),
+ bytes_written);
+
+ WriteToLog(stats);
+
+ // Close file
+ if (gpu_log_file) {
+ gpu_log_file->Flush();
+ gpu_log_file->Close();
+ gpu_log_file.reset();
+ }
+
+ // Note: Crash handler is NOT shut down here - it remains active throughout app lifetime
+ // It will be shut down when EmulationSession is destroyed
+
+ initialized = false;
+ LOG_INFO(Render_Vulkan, "[GPU Logging] Shutdown complete");
+}
+
+void GPULogger::LogVulkanCall(const std::string& call_name, const std::string& params,
+ int result) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls) {
+ return;
+ }
+
+ // Only log all calls in Verbose or All mode
+ if (current_level != LogLevel::Verbose && current_level != LogLevel::All) {
+ // In Standard mode, only log important calls
+ if (call_name.find("vkCmd") == std::string::npos &&
+ call_name.find("vkCreate") == std::string::npos &&
+ call_name.find("vkDestroy") == std::string::npos) {
+ return;
+ }
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+ const auto thread_id = static_cast(std::hash{}(std::this_thread::get_id()));
+
+ // Add to ring buffer
+ {
+ std::lock_guard lock(ring_buffer_mutex);
+ call_ring_buffer[ring_buffer_index] = {
+ .timestamp = timestamp,
+ .call_name = call_name,
+ .parameters = params,
+ .result = result,
+ .thread_id = thread_id,
+ };
+ ring_buffer_index = (ring_buffer_index + 1) % ring_buffer_size;
+ total_vulkan_calls++;
+ }
+
+ // Log to file
+ const auto log_entry =
+ fmt::format("[{}] [Vulkan] [Thread:{}] {}({}) -> {}\n", FormatTimestamp(timestamp),
+ thread_id, call_name, params, result);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogMemoryAllocation(uintptr_t memory, u64 size, u32 memory_flags) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_memory) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const bool is_device_local = (memory_flags & 0x1) != 0; // VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
+ const bool is_host_visible = (memory_flags & 0x2) != 0; // VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
+
+ {
+ std::lock_guard lock(memory_mutex);
+ memory_allocations[memory] = {
+ .memory_handle = memory,
+ .size = size,
+ .memory_flags = memory_flags,
+ .timestamp = timestamp,
+ .is_device_local = is_device_local,
+ .is_host_visible = is_host_visible,
+ };
+
+ total_allocations++;
+ current_allocated_bytes += size;
+ if (current_allocated_bytes > peak_allocated_bytes) {
+ peak_allocated_bytes = current_allocated_bytes;
+ }
+ }
+
+ const auto log_entry = fmt::format(
+ "[{}] [Memory] Allocated {} at 0x{:x} (Device:{}, Host:{})\n", FormatTimestamp(timestamp),
+ FormatMemorySize(size), memory, is_device_local ? "Yes" : "No",
+ is_host_visible ? "Yes" : "No");
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogMemoryDeallocation(uintptr_t memory) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_memory) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ u64 size = 0;
+ {
+ std::lock_guard lock(memory_mutex);
+ auto it = memory_allocations.find(memory);
+ if (it != memory_allocations.end()) {
+ size = it->second.size;
+ current_allocated_bytes -= size;
+ memory_allocations.erase(it);
+ total_deallocations++;
+ }
+ }
+
+ if (size > 0) {
+ const auto log_entry =
+ fmt::format("[{}] [Memory] Deallocated {} at 0x{:x}\n", FormatTimestamp(timestamp),
+ FormatMemorySize(size), memory);
+ WriteToLog(log_entry);
+ }
+}
+
+void GPULogger::LogShaderCompilation(const std::string& shader_name,
+ const std::string& shader_info,
+ std::span spirv_code) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!dump_shaders && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [Shader] Compiled: {} ({})\n",
+ FormatTimestamp(timestamp), shader_name, shader_info);
+ WriteToLog(log_entry);
+
+ // Dump SPIR-V binary if enabled and we have data
+ if (dump_shaders && !spirv_code.empty()) {
+ using namespace Common::FS;
+ const auto& log_dir = GetEdenPath(EdenPath::LogDir);
+ const auto shaders_dir = log_dir / "shaders";
+
+ // Create directory on first dump
+ if (!shader_dump_dir_created) {
+ [[maybe_unused]] const bool created = CreateDir(shaders_dir);
+ shader_dump_dir_created = true;
+ }
+
+ // Write SPIR-V binary file
+ const auto shader_path = shaders_dir / fmt::format("{}.spv", shader_name);
+ auto shader_file = std::make_unique(
+ shader_path, FileAccessMode::Write, FileType::BinaryFile);
+
+ if (shader_file->IsOpen()) {
+ const size_t bytes_to_write = spirv_code.size() * sizeof(u32);
+ static_cast(shader_file->WriteSpan(spirv_code));
+ shader_file->Close();
+
+ const auto dump_log = fmt::format("[{}] [Shader] Dumped SPIR-V: {} ({} bytes)\n",
+ FormatTimestamp(timestamp), shader_path.string(), bytes_to_write);
+ WriteToLog(dump_log);
+ } else {
+ LOG_WARNING(Render_Vulkan, "[GPU Logging] Failed to dump shader: {}", shader_path.string());
+ }
+ }
+}
+
+void GPULogger::LogPipelineStateChange(const std::string& state_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ // Store pipeline state for crash dumps
+ {
+ std::lock_guard lock(state_mutex);
+ stored_pipeline_state = state_info;
+ }
+
+ if (current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry =
+ fmt::format("[{}] [Pipeline] State change: {}\n", FormatTimestamp(timestamp), state_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogDriverDebugInfo(const std::string& debug_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ // Store driver debug info for crash dumps
+ {
+ std::lock_guard lock(state_mutex);
+ stored_driver_debug_info = debug_info;
+ }
+
+ if (!capture_driver_debug) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry =
+ fmt::format("[{}] [Driver] {}\n", FormatTimestamp(timestamp), debug_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogExtensionUsage(const std::string& extension_name, const std::string& function_name) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ bool is_first_use = false;
+ {
+ std::lock_guard lock(extension_mutex);
+ auto [iter, inserted] = used_extensions.insert(extension_name);
+ is_first_use = inserted;
+ }
+
+ if (is_first_use) {
+ const auto log_entry = fmt::format("[{}] [Extension] First use of {} in {}\n",
+ FormatTimestamp(timestamp), extension_name, function_name);
+ WriteToLog(log_entry);
+ LOG_INFO(Render_Vulkan, "[GPU Logging] First use of extension {} in {}",
+ extension_name, function_name);
+ } else if (current_level >= LogLevel::Verbose) {
+ const auto log_entry = fmt::format("[{}] [Extension] {} used in {}\n",
+ FormatTimestamp(timestamp), extension_name, function_name);
+ WriteToLog(log_entry);
+ }
+}
+
+void GPULogger::LogRenderPassBegin(const std::string& render_pass_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [RenderPass] Begin: {}\n",
+ FormatTimestamp(timestamp), render_pass_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogRenderPassEnd() {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [RenderPass] End\n", FormatTimestamp(timestamp));
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogPipelineBind(bool is_compute, const std::string& pipeline_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const char* pipeline_type = is_compute ? "Compute" : "Graphics";
+ const auto log_entry = fmt::format("[{}] [Pipeline] Bind {} pipeline: {}\n",
+ FormatTimestamp(timestamp), pipeline_type, pipeline_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogDescriptorSetBind(const std::string& descriptor_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [Descriptor] Bind: {}\n",
+ FormatTimestamp(timestamp), descriptor_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogPipelineBarrier(const std::string& barrier_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [Barrier] {}\n",
+ FormatTimestamp(timestamp), barrier_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogImageOperation(const std::string& operation, const std::string& image_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [Image] {}: {}\n",
+ FormatTimestamp(timestamp), operation, image_info);
+ WriteToLog(log_entry);
+}
+
+void GPULogger::LogClearOperation(const std::string& clear_info) {
+ if (!initialized || current_level == LogLevel::Off) {
+ return;
+ }
+
+ if (!track_vulkan_calls && current_level < LogLevel::Verbose) {
+ return;
+ }
+
+ const auto timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+
+ const auto log_entry = fmt::format("[{}] [Clear] {}\n",
+ FormatTimestamp(timestamp), clear_info);
+ WriteToLog(log_entry);
+}
+
+GPUStateSnapshot GPULogger::GetCurrentSnapshot() {
+ GPUStateSnapshot snapshot;
+ snapshot.timestamp = std::chrono::duration_cast(
+ std::chrono::steady_clock::now().time_since_epoch());
+ snapshot.driver_type = detected_driver;
+
+ // Capture recent Vulkan calls
+ {
+ std::lock_guard lock(ring_buffer_mutex);
+ snapshot.recent_calls.reserve(ring_buffer_size);
+
+ // Copy from current position to end
+ for (size_t i = ring_buffer_index; i < ring_buffer_size; ++i) {
+ if (!call_ring_buffer[i].call_name.empty()) {
+ snapshot.recent_calls.push_back(call_ring_buffer[i]);
+ }
+ }
+
+ // Copy from beginning to current position
+ for (size_t i = 0; i < ring_buffer_index; ++i) {
+ if (!call_ring_buffer[i].call_name.empty()) {
+ snapshot.recent_calls.push_back(call_ring_buffer[i]);
+ }
+ }
+ }
+
+ // Capture memory status
+ {
+ std::lock_guard lock(memory_mutex);
+ snapshot.memory_status = fmt::format(
+ "Total Allocations: {}\n"
+ "Current Usage: {}\n"
+ "Peak Usage: {}\n"
+ "Active Allocations: {}\n",
+ total_allocations, FormatMemorySize(current_allocated_bytes),
+ FormatMemorySize(peak_allocated_bytes), memory_allocations.size());
+ }
+
+ // Capture stored pipeline and driver debug info
+ {
+ std::lock_guard lock(state_mutex);
+ snapshot.pipeline_state = stored_pipeline_state.empty() ?
+ "No pipeline state logged yet" : stored_pipeline_state;
+ snapshot.driver_debug_info = stored_driver_debug_info.empty() ?
+ "No driver debug info logged yet" : stored_driver_debug_info;
+ }
+
+ return snapshot;
+}
+
+void GPULogger::DumpStateToFile(const std::string& crash_reason) {
+ using namespace Common::FS;
+ const auto& log_dir = GetEdenPath(EdenPath::LogDir);
+ const auto crashes_dir = log_dir / "gpu_crashes";
+ [[maybe_unused]] const bool crashes_dir_created = CreateDir(crashes_dir);
+
+ // Generate crash dump filename with timestamp
+ const auto now = std::chrono::system_clock::now();
+ const auto timestamp = std::chrono::duration_cast(
+ now.time_since_epoch()).count();
+ const auto crash_dump_path = crashes_dir / fmt::format("crash_{}.gpu-dump", timestamp);
+
+ auto crash_file =
+ std::make_unique(crash_dump_path, FileAccessMode::Write, FileType::TextFile);
+
+ if (!crash_file->IsOpen()) {
+ LOG_ERROR(Render_Vulkan, "[GPU Logging] Failed to create crash dump file");
+ return;
+ }
+
+ auto snapshot = GetCurrentSnapshot();
+
+ const char* driver_name = "Unknown";
+ switch (snapshot.driver_type) {
+ case DriverType::Turnip:
+ driver_name = "Turnip (Mesa Freedreno)";
+ break;
+ case DriverType::Qualcomm:
+ driver_name = "Qualcomm Proprietary";
+ break;
+ default:
+ driver_name = "Unknown";
+ break;
+ }
+
+ // Write crash dump header
+ const auto header = fmt::format(
+ "=== GPU CRASH DUMP ===\n"
+ "Timestamp: {}\n"
+ "Reason: {}\n"
+ "Driver: {}\n"
+ "\n",
+ FormatTimestamp(snapshot.timestamp), crash_reason, driver_name);
+ static_cast(crash_file->WriteString(header));
+
+ // Write recent Vulkan calls
+ static_cast(crash_file->WriteString(fmt::format("=== RECENT VULKAN API CALLS (Last {}) ===\n",
+ snapshot.recent_calls.size())));
+ for (const auto& call : snapshot.recent_calls) {
+ const auto call_str =
+ fmt::format("[{}] [Thread:{}] {}({}) -> {}\n", FormatTimestamp(call.timestamp),
+ call.thread_id, call.call_name, call.parameters, call.result);
+ static_cast(crash_file->WriteString(call_str));
+ }
+ static_cast(crash_file->WriteString("\n"));
+
+ // Write memory status
+ static_cast(crash_file->WriteString("=== MEMORY STATUS ===\n"));
+ static_cast(crash_file->WriteString(snapshot.memory_status));
+ static_cast(crash_file->WriteString("\n"));
+
+ // Write pipeline state
+ static_cast(crash_file->WriteString("=== PIPELINE STATE ===\n"));
+ static_cast(crash_file->WriteString(snapshot.pipeline_state));
+ static_cast(crash_file->WriteString("\n"));
+
+ // Write driver debug info
+ static_cast(crash_file->WriteString("=== DRIVER DEBUG INFO ===\n"));
+ static_cast(crash_file->WriteString(snapshot.driver_debug_info));
+ static_cast(crash_file->WriteString("\n"));
+
+ crash_file->Flush();
+ crash_file->Close();
+
+ LOG_CRITICAL(Render_Vulkan, "[GPU Logging] Crash dump written to: {}",
+ crash_dump_path.string());
+}
+
+void GPULogger::SetLogLevel(LogLevel level) {
+ current_level = level;
+}
+
+void GPULogger::EnableVulkanCallTracking(bool enabled) {
+ track_vulkan_calls = enabled;
+}
+
+void GPULogger::EnableShaderDumps(bool enabled) {
+ dump_shaders = enabled;
+}
+
+void GPULogger::EnableMemoryTracking(bool enabled) {
+ track_memory = enabled;
+}
+
+void GPULogger::EnableDriverDebugInfo(bool enabled) {
+ capture_driver_debug = enabled;
+}
+
+void GPULogger::SetRingBufferSize(size_t entries) {
+ std::lock_guard lock(ring_buffer_mutex);
+ ring_buffer_size = entries;
+ call_ring_buffer.resize(entries);
+ ring_buffer_index = 0;
+}
+
+LogLevel GPULogger::GetLogLevel() const {
+ return current_level;
+}
+
+DriverType GPULogger::GetDriverType() const {
+ return detected_driver;
+}
+
+std::string GPULogger::GetStatistics() const {
+ std::lock_guard lock(memory_mutex);
+ return fmt::format(
+ "Vulkan Calls: {}, Allocations: {}, Deallocations: {}, "
+ "Current Memory: {}, Peak Memory: {}",
+ total_vulkan_calls, total_allocations, total_deallocations,
+ FormatMemorySize(current_allocated_bytes), FormatMemorySize(peak_allocated_bytes));
+}
+
+bool GPULogger::IsInitialized() const {
+ return initialized;
+}
+
+void GPULogger::WriteToLog(const std::string& message) {
+ if (!gpu_log_file || !gpu_log_file->IsOpen()) {
+ return;
+ }
+
+ std::lock_guard lock(file_mutex);
+ bytes_written += gpu_log_file->WriteString(message);
+
+ // Flush on errors or if we've written a lot
+ using namespace Common::Literals;
+ if (bytes_written % (1_MiB) == 0) {
+ gpu_log_file->Flush();
+ }
+}
+
+std::string GPULogger::FormatTimestamp(std::chrono::microseconds timestamp) const {
+ const auto seconds = timestamp.count() / 1000000;
+ const auto microseconds = timestamp.count() % 1000000;
+ return fmt::format("{:4d}.{:06d}", seconds, microseconds);
+}
+
+std::string GPULogger::FormatMemorySize(u64 bytes) const {
+ using namespace Common::Literals;
+ if (bytes >= 1_GiB) {
+ return fmt::format("{:.2f} GiB", static_cast(bytes) / (1_GiB));
+ } else if (bytes >= 1_MiB) {
+ return fmt::format("{:.2f} MiB", static_cast(bytes) / (1_MiB));
+ } else if (bytes >= 1_KiB) {
+ return fmt::format("{:.2f} KiB", static_cast(bytes) / (1_KiB));
+ } else {
+ return fmt::format("{} B", bytes);
+ }
+}
+
+} // namespace GPU::Logging
diff --git a/src/video_core/gpu_logging/gpu_logging.h b/src/video_core/gpu_logging/gpu_logging.h
new file mode 100644
index 0000000000..c248e9b036
--- /dev/null
+++ b/src/video_core/gpu_logging/gpu_logging.h
@@ -0,0 +1,199 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common/common_types.h"
+
+// Forward declarations
+namespace Common::FS {
+class IOFile;
+}
+
+namespace Vulkan {
+class Device;
+}
+
+namespace GPU::Logging {
+
+enum class LogLevel : u8 {
+ Off = 0,
+ Errors = 1,
+ Standard = 2,
+ Verbose = 3,
+ All = 4,
+};
+
+enum class DriverType : u8 {
+ Unknown,
+ Turnip, // Mesa Turnip driver
+ Qualcomm, // Qualcomm proprietary driver
+};
+
+// Ring buffer entry for tracking Vulkan API calls
+struct VulkanCallEntry {
+ std::chrono::microseconds timestamp;
+ std::string call_name; // e.g., "vkCmdDraw", "vkBeginRenderPass"
+ std::string parameters; // Serialized parameters
+ int result; // VkResult return code
+ u32 thread_id;
+};
+
+// GPU memory allocation entry
+struct MemoryAllocationEntry {
+ uintptr_t memory_handle;
+ u64 size;
+ u32 memory_flags;
+ std::chrono::microseconds timestamp;
+ bool is_device_local;
+ bool is_host_visible;
+};
+
+// GPU state snapshot for crash dumps
+struct GPUStateSnapshot {
+ std::vector recent_calls; // Last N API calls
+ std::vector active_shaders; // Currently bound shaders
+ std::string pipeline_state; // Current pipeline state
+ std::string memory_status; // Current memory allocations
+ std::string driver_debug_info; // Driver-specific debug data
+ std::chrono::microseconds timestamp;
+ DriverType driver_type;
+};
+
+/// Main GPU logging system singleton
+class GPULogger {
+public:
+ static GPULogger& GetInstance();
+
+ // Prevent copying
+ GPULogger(const GPULogger&) = delete;
+ GPULogger& operator=(const GPULogger&) = delete;
+
+ // Initialization and control
+ void Initialize(LogLevel level, DriverType detected_driver = DriverType::Unknown);
+ void Shutdown();
+
+ // Logging API
+ void LogVulkanCall(const std::string& call_name, const std::string& params, int result);
+ void LogMemoryAllocation(uintptr_t memory, u64 size, u32 memory_flags);
+ void LogMemoryDeallocation(uintptr_t memory);
+ void LogShaderCompilation(const std::string& shader_name, const std::string& shader_info,
+ std::span spirv_code = {});
+ void LogPipelineStateChange(const std::string& state_info);
+ void LogDriverDebugInfo(const std::string& debug_info);
+
+ // Extension usage tracking
+ void LogExtensionUsage(const std::string& extension_name, const std::string& function_name);
+
+ // Render pass logging
+ void LogRenderPassBegin(const std::string& render_pass_info);
+ void LogRenderPassEnd();
+
+ // Pipeline binding logging
+ void LogPipelineBind(bool is_compute, const std::string& pipeline_info);
+
+ // Descriptor set binding logging
+ void LogDescriptorSetBind(const std::string& descriptor_info);
+
+ // Pipeline barrier logging
+ void LogPipelineBarrier(const std::string& barrier_info);
+
+ // Image operation logging
+ void LogImageOperation(const std::string& operation, const std::string& image_info);
+
+ // Clear operation logging
+ void LogClearOperation(const std::string& clear_info);
+
+ // Crash handling
+ GPUStateSnapshot GetCurrentSnapshot();
+ void DumpStateToFile(const std::string& crash_reason);
+
+ // Settings
+ void SetLogLevel(LogLevel level);
+ void EnableVulkanCallTracking(bool enabled);
+ void EnableShaderDumps(bool enabled);
+ void EnableMemoryTracking(bool enabled);
+ void EnableDriverDebugInfo(bool enabled);
+ void SetRingBufferSize(size_t entries);
+
+ // Query
+ LogLevel GetLogLevel() const;
+ DriverType GetDriverType() const;
+ std::string GetStatistics() const;
+ bool IsInitialized() const;
+
+private:
+ GPULogger();
+ ~GPULogger();
+
+ // Helper functions
+ void WriteToLog(const std::string& message);
+ void RotateLogFile();
+ std::string FormatTimestamp(std::chrono::microseconds timestamp) const;
+ std::string FormatMemorySize(u64 bytes) const;
+
+ // State
+ bool initialized = false;
+ LogLevel current_level = LogLevel::Off;
+ DriverType detected_driver = DriverType::Unknown;
+
+ // Ring buffer for API calls
+ std::vector call_ring_buffer;
+ size_t ring_buffer_index = 0;
+ size_t ring_buffer_size = 512;
+ mutable std::mutex ring_buffer_mutex;
+
+ // Memory tracking
+ std::unordered_map memory_allocations;
+ mutable std::mutex memory_mutex;
+
+ // Statistics
+ u64 total_vulkan_calls = 0;
+ u64 total_allocations = 0;
+ u64 total_deallocations = 0;
+ u64 current_allocated_bytes = 0;
+ u64 peak_allocated_bytes = 0;
+
+ // File backend for GPU logs
+ std::unique_ptr gpu_log_file;
+ mutable std::mutex file_mutex;
+ u64 bytes_written = 0;
+
+ // Feature flags
+ bool track_vulkan_calls = true;
+ bool dump_shaders = false;
+ bool track_memory = false;
+ bool capture_driver_debug = false;
+
+ // Extension usage tracking
+ std::set used_extensions;
+ mutable std::mutex extension_mutex;
+
+ // Shader dump directory (created on demand)
+ bool shader_dump_dir_created = false;
+
+ // Stored state for crash dumps
+ std::string stored_driver_debug_info;
+ std::string stored_pipeline_state;
+ mutable std::mutex state_mutex;
+};
+
+// Helper to get stage name from index
+inline const char* GetShaderStageName(size_t stage_index) {
+ static constexpr std::array stage_names{
+ "vertex", "tess_control", "tess_eval", "geometry", "fragment"
+ };
+ return stage_index < stage_names.size() ? stage_names[stage_index] : "unknown";
+}
+
+} // namespace GPU::Logging
diff --git a/src/video_core/gpu_logging/gpu_state_capture.cpp b/src/video_core/gpu_logging/gpu_state_capture.cpp
new file mode 100644
index 0000000000..19e655a71b
--- /dev/null
+++ b/src/video_core/gpu_logging/gpu_state_capture.cpp
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "video_core/gpu_logging/gpu_state_capture.h"
+#include
+
+namespace GPU::Logging {
+
+GPUStateSnapshot GPUStateCapture::CaptureState() {
+ return GPULogger::GetInstance().GetCurrentSnapshot();
+}
+
+std::string GPUStateCapture::SerializeState(const GPUStateSnapshot& snapshot) {
+ std::string result;
+
+ result += "=== GPU STATE SNAPSHOT ===\n\n";
+
+ result += fmt::format("Driver: {}\n", static_cast(snapshot.driver_type));
+ result += fmt::format("Recent Calls: {}\n\n", snapshot.recent_calls.size());
+
+ result += "=== RECENT VULKAN CALLS ===\n";
+ for (const auto& call : snapshot.recent_calls) {
+ result += fmt::format("{}: {}({}) -> {}\n", call.timestamp.count(), call.call_name,
+ call.parameters, call.result);
+ }
+
+ result += "\n=== MEMORY STATUS ===\n";
+ result += snapshot.memory_status;
+
+ result += "\n=== PIPELINE STATE ===\n";
+ result += snapshot.pipeline_state;
+
+ result += "\n=== DRIVER DEBUG INFO ===\n";
+ result += snapshot.driver_debug_info;
+
+ return result;
+}
+
+void GPUStateCapture::WriteCrashDump(const std::string& crash_reason) {
+ GPULogger::GetInstance().DumpStateToFile(crash_reason);
+}
+
+} // namespace GPU::Logging
diff --git a/src/video_core/gpu_logging/gpu_state_capture.h b/src/video_core/gpu_logging/gpu_state_capture.h
new file mode 100644
index 0000000000..aca06785ed
--- /dev/null
+++ b/src/video_core/gpu_logging/gpu_state_capture.h
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include "video_core/gpu_logging/gpu_logging.h"
+
+namespace GPU::Logging {
+
+class GPUStateCapture {
+public:
+ // Capture current GPU state from logging system
+ static GPUStateSnapshot CaptureState();
+
+ // Serialize state to human-readable format
+ static std::string SerializeState(const GPUStateSnapshot& snapshot);
+
+ // Write detailed crash dump (implemented in GPULogger)
+ static void WriteCrashDump(const std::string& crash_reason);
+};
+
+} // namespace GPU::Logging
diff --git a/src/video_core/gpu_logging/qualcomm_debug.cpp b/src/video_core/gpu_logging/qualcomm_debug.cpp
new file mode 100644
index 0000000000..4862bc2349
--- /dev/null
+++ b/src/video_core/gpu_logging/qualcomm_debug.cpp
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "video_core/gpu_logging/qualcomm_debug.h"
+#include "common/logging/log.h"
+
+namespace GPU::Logging::Qualcomm {
+
+bool QualcommDebugger::is_initialized = false;
+
+void QualcommDebugger::Initialize() {
+ if (is_initialized) {
+ return;
+ }
+
+ is_initialized = true;
+ LOG_INFO(Render_Vulkan, "[Qualcomm Debug] Initialized (stub)");
+}
+
+std::string QualcommDebugger::GetDebugInfo() {
+ // Stub for future Qualcomm proprietary driver debug extension support
+ // This requires libadrenotools integration and Qualcomm-specific APIs
+ return "Qualcomm debug info not yet implemented";
+}
+
+} // namespace GPU::Logging::Qualcomm
diff --git a/src/video_core/gpu_logging/qualcomm_debug.h b/src/video_core/gpu_logging/qualcomm_debug.h
new file mode 100644
index 0000000000..c54ae5ed0c
--- /dev/null
+++ b/src/video_core/gpu_logging/qualcomm_debug.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+
+namespace GPU::Logging::Qualcomm {
+
+class QualcommDebugger {
+public:
+ // Initialize Qualcomm debugging (stub for future implementation)
+ static void Initialize();
+
+ // Get debug information from Qualcomm driver
+ static std::string GetDebugInfo();
+
+private:
+ static bool is_initialized;
+};
+
+} // namespace GPU::Logging::Qualcomm
diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
index 2d9c5d4148..96a9fe59e7 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -8,6 +8,7 @@
#include
#include
+#include
#include "video_core/renderer_vulkan/pipeline_helper.h"
#include "video_core/renderer_vulkan/pipeline_statistics.h"
@@ -20,6 +21,8 @@
#include "video_core/shader_notify.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#include "video_core/gpu_logging/gpu_logging.h"
+#include "common/settings.h"
namespace Vulkan {
@@ -82,6 +85,13 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
},
*pipeline_cache);
+ // Log compute pipeline creation
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogPipelineStateChange(
+ "ComputePipeline created"
+ );
+ }
+
if (pipeline_statistics) {
pipeline_statistics->Collect(*pipeline);
}
@@ -207,6 +217,13 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
build_condvar.wait(lock, [this] { return is_built.load(std::memory_order::relaxed); });
});
}
+
+ // Log compute pipeline binding
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogPipelineBind(true, "compute pipeline");
+ }
+
const void* const descriptor_data{guest_descriptor_queue.UpdateData()};
const bool is_rescaling = !info.texture_descriptors.empty() || !info.image_descriptors.empty();
scheduler.Record([this, descriptor_data, is_rescaling,
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
index fca235b58e..d36553da4a 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -10,6 +10,7 @@
#include
#include
+#include
#include "video_core/renderer_vulkan/pipeline_helper.h"
@@ -25,6 +26,8 @@
#include "video_core/shader_notify.h"
#include "video_core/texture_cache/texture_cache.h"
#include "video_core/vulkan_common/vulkan_device.h"
+#include "video_core/gpu_logging/gpu_logging.h"
+#include "common/settings.h"
#if defined(_MSC_VER) && defined(NDEBUG)
#define LAMBDA_FORCEINLINE [[msvc::forceinline]]
@@ -513,6 +516,14 @@ void GraphicsPipeline::ConfigureDraw(const RescalingPushConstant& rescaling,
const bool is_rescaling{texture_cache.IsRescaling()};
const bool update_rescaling{scheduler.UpdateRescaling(is_rescaling)};
const bool bind_pipeline{scheduler.UpdateGraphicsPipeline(this)};
+
+ // Log graphics pipeline binding
+ if (bind_pipeline && Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ const std::string pipeline_info = fmt::format("hash=0x{:016x}", key.Hash());
+ GPU::Logging::GPULogger::GetInstance().LogPipelineBind(false, pipeline_info);
+ }
+
const void* const descriptor_data{guest_descriptor_queue.UpdateData()};
scheduler.Record([this, descriptor_data, bind_pipeline, rescaling_data = rescaling.Data(),
is_rescaling, update_rescaling,
@@ -954,6 +965,16 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
.basePipelineIndex = 0,
},
*pipeline_cache);
+
+ // Log graphics pipeline creation
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ const std::string pipeline_info = fmt::format(
+ "GraphicsPipeline created: stages={}, attachments={}",
+ shader_stages.size(),
+ color_blend_ci.attachmentCount
+ );
+ GPU::Logging::GPULogger::GetInstance().LogPipelineStateChange(pipeline_info);
+ }
}
void GraphicsPipeline::Validate() {
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 6207e127c1..f3dd0f90d8 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -42,6 +43,7 @@
#include "video_core/surface.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#include "video_core/gpu_logging/gpu_logging.h"
namespace Vulkan {
@@ -724,6 +726,17 @@ std::unique_ptr PipelineCache::CreateGraphicsPipeline(
const std::vector code{EmitSPIRV(profile, runtime_info, program, binding, this->optimize_spirv_output)};
device.SaveShader(code);
modules[stage_index] = BuildShader(device, code);
+
+ // Log shader compilation to GPU logger (with SPIR-V binary dump if enabled)
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ static constexpr std::array stage_names{"vertex", "tess_control", "tess_eval", "geometry", "fragment"};
+ const std::string shader_name = fmt::format("shader_{:016x}_{}", key.unique_hashes[index], stage_names[stage_index]);
+ const std::string shader_info = fmt::format("SPIR-V size: {} bytes, hash: {:016x}",
+ code.size() * sizeof(u32), key.unique_hashes[index]);
+ GPU::Logging::GPULogger::GetInstance().LogShaderCompilation(shader_name, shader_info,
+ std::span(code.data(), code.size()));
+ }
+
if (device.HasDebuggingToolAttached()) {
const std::string name{fmt::format("Shader {:016x}", key.unique_hashes[index])};
modules[stage_index].SetObjectNameEXT(name.c_str());
@@ -831,6 +844,16 @@ std::unique_ptr PipelineCache::CreateComputePipeline(
const std::vector code{EmitSPIRV(profile, program, this->optimize_spirv_output)};
device.SaveShader(code);
vk::ShaderModule spv_module{BuildShader(device, code)};
+
+ // Log compute shader compilation to GPU logger (with SPIR-V binary dump if enabled)
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ const std::string shader_name = fmt::format("shader_{:016x}_compute", key.unique_hash);
+ const std::string shader_info = fmt::format("SPIR-V size: {} bytes, hash: {:016x}",
+ code.size() * sizeof(u32), key.unique_hash);
+ GPU::Logging::GPULogger::GetInstance().LogShaderCompilation(shader_name, shader_info,
+ std::span(code.data(), code.size()));
+ }
+
if (device.HasDebuggingToolAttached()) {
const auto name{fmt::format("Shader {:016x}", key.unique_hash)};
spv_module.SetObjectNameEXT(name.c_str());
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index c3a5ed391b..60b899a811 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -9,6 +9,8 @@
#include
#include
+#include
+
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "common/assert.h"
@@ -16,6 +18,7 @@
#include "common/scope_exit.h"
#include "common/settings.h"
#include "video_core/buffer_cache/buffer_cache.h"
+#include "video_core/gpu_logging/gpu_logging.h"
#include "video_core/control/channel_state.h"
#include "video_core/engines/draw_manager.h"
#include "video_core/engines/kepler_compute.h"
@@ -277,6 +280,20 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
}
});
}
+
+ // Log draw call
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ const std::string params = is_indexed ?
+ fmt::format("vertices={}, instances={}, firstIndex={}, baseVertex={}, baseInstance={}",
+ draw_params.num_vertices, draw_params.num_instances,
+ draw_params.first_index, draw_params.base_vertex, draw_params.base_instance) :
+ fmt::format("vertices={}, instances={}, firstVertex={}, firstInstance={}",
+ draw_params.num_vertices, draw_params.num_instances,
+ draw_params.base_vertex, draw_params.base_instance);
+ GPU::Logging::GPULogger::GetInstance().LogVulkanCall(
+ is_indexed ? "vkCmdDrawIndexed" : "vkCmdDraw", params, VK_SUCCESS);
+ }
});
}
@@ -324,6 +341,16 @@ void RasterizerVulkan::DrawIndirect() {
static_cast(params.stride));
}
});
+
+ // Log indirect draw call
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ const std::string log_params = fmt::format("drawCount={}, stride={}",
+ params.max_draw_counts, params.stride);
+ GPU::Logging::GPULogger::GetInstance().LogVulkanCall(
+ params.is_indexed ? "vkCmdDrawIndexedIndirect" : "vkCmdDrawIndirect",
+ log_params, VK_SUCCESS);
+ }
});
buffer_cache.SetDrawIndirect(nullptr);
}
@@ -568,6 +595,15 @@ void RasterizerVulkan::DispatchCompute() {
scheduler.Record([](vk::CommandBuffer cmdbuf) { cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, READ_BARRIER); });
scheduler.Record([dim](vk::CommandBuffer cmdbuf) { cmdbuf.Dispatch(dim[0], dim[1], dim[2]); });
+
+ // Log compute dispatch
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ const std::string params = fmt::format("groupCountX={}, groupCountY={}, groupCountZ={}",
+ dim[0], dim[1], dim[2]);
+ GPU::Logging::GPULogger::GetInstance().LogVulkanCall(
+ "vkCmdDispatch", params, VK_SUCCESS);
+ }
}
void RasterizerVulkan::ResetCounter(VideoCommon::QueryType type) {
@@ -1066,6 +1102,11 @@ void RasterizerVulkan::HandleTransformFeedback() {
query_cache.CounterEnable(VideoCommon::QueryType::StreamingByteCount,
regs.transform_feedback_enabled);
if (regs.transform_feedback_enabled != 0) {
+ // Log extension usage for transform feedback
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogExtensionUsage(
+ "VK_EXT_transform_feedback", "HandleTransformFeedback");
+ }
UNIMPLEMENTED_IF(regs.IsShaderConfigEnabled(Maxwell::ShaderType::TessellationInit) ||
regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation));
}
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index e19029812b..e526d606dc 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -9,9 +9,13 @@
#include
#include
+#include
+
#include "video_core/renderer_vulkan/vk_query_cache.h"
+#include "common/settings.h"
#include "common/thread.h"
+#include "video_core/gpu_logging/gpu_logging.h"
#include "video_core/renderer_vulkan/vk_command_pool.h"
#include "video_core/renderer_vulkan/vk_graphics_pipeline.h"
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
@@ -114,6 +118,15 @@ void Scheduler::RequestRenderpass(const Framebuffer* framebuffer) {
state.framebuffer = framebuffer_handle;
state.render_area = render_area;
+ // Log render pass begin
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ const std::string render_pass_info = fmt::format(
+ "renderArea={}x{}, numImages={}",
+ render_area.width, render_area.height, framebuffer->NumImages());
+ GPU::Logging::GPULogger::GetInstance().LogRenderPassBegin(render_pass_info);
+ }
+
Record([renderpass, framebuffer_handle, render_area](vk::CommandBuffer cmdbuf) {
const VkRenderPassBeginInfo renderpass_bi{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
@@ -270,6 +283,12 @@ u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_se
switch (const VkResult result = master_semaphore->SubmitQueue(
cmdbuf, upload_cmdbuf, signal_semaphore, wait_semaphore, signal_value)) {
case VK_SUCCESS:
+ // Log successful queue submission
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogVulkanCall(
+ "vkQueueSubmit", "", VK_SUCCESS);
+ }
break;
case VK_ERROR_DEVICE_LOST:
device.ReportLoss();
@@ -305,6 +324,12 @@ void Scheduler::EndRenderPass()
return;
}
+ // Log render pass end
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogRenderPassEnd();
+ }
+
query_cache->CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, false);
query_cache->NotifySegment(false);
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 6ce7433240..39a43d5950 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -16,6 +16,7 @@
#include "common/settings.h"
#include "video_core/renderer_vulkan/vk_texture_cache.h"
+#include "video_core/gpu_logging/gpu_logging.h"
#include "video_core/engines/fermi_2d.h"
#include "video_core/renderer_vulkan/blit_image.h"
@@ -2304,6 +2305,11 @@ Sampler::Sampler(TextureCacheRuntime& runtime, const Tegra::Texture::TSCEntry& t
const void* pnext = nullptr;
if (has_custom_border_colors) {
pnext = &border_ci;
+ // Log extension usage for custom border color
+ if (Settings::values.gpu_logging_enabled.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogExtensionUsage(
+ "VK_EXT_custom_border_color", "Sampler::Sampler");
+ }
}
const VkSamplerReductionModeCreateInfoEXT reduction_ci{
.sType = VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT,
diff --git a/src/video_core/vulkan_common/vulkan_debug_callback.cpp b/src/video_core/vulkan_common/vulkan_debug_callback.cpp
index 448df2d3ab..cd6653c86e 100644
--- a/src/video_core/vulkan_common/vulkan_debug_callback.cpp
+++ b/src/video_core/vulkan_common/vulkan_debug_callback.cpp
@@ -1,12 +1,26 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
#include
#include "common/logging/log.h"
+#include "common/settings.h"
#include "video_core/vulkan_common/vulkan_debug_callback.h"
+#include "video_core/gpu_logging/gpu_logging.h"
namespace Vulkan {
namespace {
+
+// Helper to get message type as string for GPU logging
+const char* GetMessageTypeName(VkDebugUtilsMessageTypeFlagsEXT type) {
+ if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) {
+ return "Validation";
+ } else if (type & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) {
+ return "Performance";
+ } else {
+ return "General";
+ }
+}
+
VkBool32 DebugUtilCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT* data,
@@ -60,6 +74,28 @@ VkBool32 DebugUtilCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
} else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) {
LOG_DEBUG(Render_Vulkan, "{}", message);
}
+
+ // Route to GPU logger for tracking Vulkan validation messages
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_vulkan_calls.GetValue()) {
+ // Convert severity to result code for logging (negative = error)
+ int result_code = 0;
+ if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
+ result_code = -1;
+ } else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
+ result_code = -2;
+ }
+
+ // Get message ID name or use generic name
+ const char* call_name = data->pMessageIdName ? data->pMessageIdName : "VulkanDebug";
+
+ GPU::Logging::GPULogger::GetInstance().LogVulkanCall(
+ call_name,
+ std::string(GetMessageTypeName(type)) + ": " + std::string(message),
+ result_code
+ );
+ }
+
return VK_FALSE;
}
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 8c596fe347..0b3aea6d2e 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -13,6 +13,8 @@
#include
#include
+#include
+
#include "common/assert.h"
#include "common/literals.h"
#include
@@ -22,6 +24,7 @@
#include "video_core/vulkan_common/vma.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#include "video_core/gpu_logging/gpu_logging.h"
#if defined(ANDROID) && defined(ARCHITECTURE_arm64)
#include
@@ -734,9 +737,13 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
};
vk::Check(vmaCreateAllocator(&allocator_info, &allocator));
+
+ // Initialize GPU logging if enabled
+ InitializeGPULogging();
}
Device::~Device() {
+ ShutdownGPULogging();
vmaDestroyAllocator(allocator);
}
@@ -1622,4 +1629,106 @@ std::vector Device::GetDeviceQueueCreateInfos() const {
return queue_cis;
}
+void Device::InitializeGPULogging() {
+ if (!Settings::values.gpu_logging_enabled.GetValue()) {
+ return;
+ }
+
+ // Detect driver type
+ const auto driver_id = GetDriverID();
+ GPU::Logging::DriverType detected_driver = GPU::Logging::DriverType::Unknown;
+
+ if (driver_id == VK_DRIVER_ID_MESA_TURNIP) {
+ detected_driver = GPU::Logging::DriverType::Turnip;
+ } else if (driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {
+ detected_driver = GPU::Logging::DriverType::Qualcomm;
+ }
+
+ // Get log level from settings
+ const auto log_level = static_cast(
+ static_cast(Settings::values.gpu_log_level.GetValue()));
+
+ // Initialize GPU logger
+ GPU::Logging::GPULogger::GetInstance().Initialize(log_level, detected_driver);
+
+ // Configure feature flags
+ GPU::Logging::GPULogger::GetInstance().EnableVulkanCallTracking(
+ Settings::values.gpu_log_vulkan_calls.GetValue());
+ GPU::Logging::GPULogger::GetInstance().EnableShaderDumps(
+ Settings::values.gpu_log_shader_dumps.GetValue());
+ GPU::Logging::GPULogger::GetInstance().EnableMemoryTracking(
+ Settings::values.gpu_log_memory_tracking.GetValue());
+ GPU::Logging::GPULogger::GetInstance().EnableDriverDebugInfo(
+ Settings::values.gpu_log_driver_debug.GetValue());
+ GPU::Logging::GPULogger::GetInstance().SetRingBufferSize(
+ Settings::values.gpu_log_ring_buffer_size.GetValue());
+
+ // Log comprehensive driver and extension information
+ if (Settings::values.gpu_log_driver_debug.GetValue()) {
+ std::string driver_info;
+
+ // Device information
+ const auto& props = properties.properties;
+ driver_info += fmt::format("Device: {}\n", props.deviceName);
+ driver_info += fmt::format("Driver Name: {}\n", properties.driver.driverName);
+ driver_info += fmt::format("Driver Info: {}\n", properties.driver.driverInfo);
+
+ // Version information
+ const u32 driver_version = props.driverVersion;
+ const u32 api_version = props.apiVersion;
+ driver_info += fmt::format("Driver Version: {}.{}.{}\n",
+ VK_API_VERSION_MAJOR(driver_version),
+ VK_API_VERSION_MINOR(driver_version),
+ VK_API_VERSION_PATCH(driver_version));
+ driver_info += fmt::format("Vulkan API Version: {}.{}.{}\n",
+ VK_API_VERSION_MAJOR(api_version),
+ VK_API_VERSION_MINOR(api_version),
+ VK_API_VERSION_PATCH(api_version));
+ driver_info += fmt::format("Driver ID: {}\n", static_cast(driver_id));
+
+ // Vendor and device IDs
+ driver_info += fmt::format("Vendor ID: 0x{:04X}\n", props.vendorID);
+ driver_info += fmt::format("Device ID: 0x{:04X}\n", props.deviceID);
+
+ // Extensions - separate QCOM extensions from others
+ driver_info += "\n=== Loaded Vulkan Extensions ===\n";
+ std::vector qcom_exts;
+ std::vector other_exts;
+
+ for (const auto& ext : loaded_extensions) {
+ if (ext.find("QCOM") != std::string::npos || ext.find("qcom") != std::string::npos) {
+ qcom_exts.push_back(ext);
+ } else {
+ other_exts.push_back(ext);
+ }
+ }
+
+ // Log QCOM extensions first
+ if (!qcom_exts.empty()) {
+ driver_info += "\nQualcomm Proprietary Extensions:\n";
+ for (const auto& ext : qcom_exts) {
+ driver_info += fmt::format(" - {}\n", ext);
+ }
+ }
+
+ // Log other extensions
+ if (!other_exts.empty()) {
+ driver_info += "\nStandard Extensions:\n";
+ for (const auto& ext : other_exts) {
+ driver_info += fmt::format(" - {}\n", ext);
+ }
+ }
+
+ driver_info += fmt::format("\nTotal Extensions Loaded: {}\n", loaded_extensions.size());
+
+ GPU::Logging::GPULogger::GetInstance().LogDriverDebugInfo(driver_info);
+ }
+}
+
+void Device::ShutdownGPULogging() {
+ if (GPU::Logging::GPULogger::GetInstance().IsInitialized()) {
+ GPU::Logging::GPULogger::GetInstance().Shutdown();
+ }
+}
+
} // namespace Vulkan
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index acb2954104..744b5f827e 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -929,6 +929,10 @@ public:
return nvidia_arch;
}
+ /// GPU logging integration
+ void InitializeGPULogging();
+ void ShutdownGPULogging();
+
private:
/// Checks if the physical device is suitable and configures the object state
/// with all necessary info about its properties.
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
index 4cd3442d97..ab93f256c0 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -22,6 +22,8 @@
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#include "video_core/gpu_logging/gpu_logging.h"
+#include "common/settings.h"
namespace Vulkan {
namespace {
@@ -107,7 +109,17 @@ namespace Vulkan {
MemoryCommit::MemoryCommit(VmaAllocator alloc, VmaAllocation a,
const VmaAllocationInfo &info) noexcept
: allocator{alloc}, allocation{a}, memory{info.deviceMemory},
- offset{info.offset}, size{info.size}, mapped_ptr{info.pMappedData} {}
+ offset{info.offset}, size{info.size}, mapped_ptr{info.pMappedData} {
+ // Log GPU memory allocation
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_memory_tracking.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogMemoryAllocation(
+ reinterpret_cast(memory),
+ static_cast(size),
+ 0 // Memory property flags (not easily available from VMA)
+ );
+ }
+ }
MemoryCommit::~MemoryCommit() { Release(); }
@@ -166,6 +178,15 @@ namespace Vulkan {
void MemoryCommit::Release() {
if (allocation && allocator) {
+ // Log GPU memory deallocation
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_memory_tracking.GetValue() &&
+ memory != VK_NULL_HANDLE) {
+ GPU::Logging::GPULogger::GetInstance().LogMemoryDeallocation(
+ reinterpret_cast(memory)
+ );
+ }
+
if (mapped_ptr) {
vmaUnmapMemory(allocator, allocation);
mapped_ptr = nullptr;
@@ -218,7 +239,19 @@ namespace Vulkan {
VkImage handle{};
VmaAllocation allocation{};
- vk::Check(vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, nullptr));
+ VmaAllocationInfo alloc_info{};
+ vk::Check(vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
+
+ // Log GPU memory allocation for images
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_memory_tracking.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogMemoryAllocation(
+ reinterpret_cast(alloc_info.deviceMemory),
+ static_cast(alloc_info.size),
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
+ );
+ }
+
return vk::Image(handle, ci.usage, *device.GetLogical(), allocator, allocation,
device.GetDispatchLoader());
}
@@ -245,6 +278,16 @@ namespace Vulkan {
vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
vmaGetAllocationMemoryProperties(allocator, allocation, &property_flags);
+ // Log GPU memory allocation for buffers
+ if (Settings::values.gpu_logging_enabled.GetValue() &&
+ Settings::values.gpu_log_memory_tracking.GetValue()) {
+ GPU::Logging::GPULogger::GetInstance().LogMemoryAllocation(
+ reinterpret_cast(alloc_info.deviceMemory),
+ static_cast(alloc_info.size),
+ property_flags
+ );
+ }
+
u8 *data = reinterpret_cast(alloc_info.pMappedData);
const std::span mapped_data = data ? std::span{data, ci.size} : std::span{};
const bool is_coherent = (property_flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;