From 1d8806cf06ce0760a90a881eb0f1b5da7d9e7c47 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Sat, 4 Jan 2025 12:42:06 +0100 Subject: [PATCH 1/4] add an option to capture GPU frame --- .../HW/Latte/Renderer/Metal/MetalRenderer.cpp | 35 +++++++++++++++++++ .../HW/Latte/Renderer/Metal/MetalRenderer.h | 15 ++++++++ src/gui/MainWindow.cpp | 21 +++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp index ee2fff89..72b756e1 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp @@ -303,6 +303,17 @@ void MetalRenderer::SwapBuffers(bool swapTV, bool swapDRC) // Debug m_performanceMonitor.ResetPerFrameData(); + + // GPU capture + if (m_capturing) + { + EndCapture(); + } + else if (m_captureFrame) + { + StartCapture(); + m_captureFrame = false; + } } void MetalRenderer::HandleScreenshotRequest(LatteTextureView* texView, bool padView) { @@ -2161,3 +2172,27 @@ void MetalRenderer::EnsureImGuiBackend() //ImGui_ImplMetal_CreateFontsTexture(m_device); } } + +void MetalRenderer::StartCapture() +{ + auto captureManager = MTL::CaptureManager::sharedCaptureManager(); + auto desc = MTL::CaptureDescriptor::alloc()->init(); + desc->setCaptureObject(m_device); + + NS::Error* error = nullptr; + captureManager->startCapture(desc, &error); + if (error) + { + cemuLog_log(LogType::Force, "Failed to start GPU capture: {}", error->localizedDescription()->utf8String()); + } + + m_capturing = true; +} + +void MetalRenderer::EndCapture() +{ + auto captureManager = MTL::CaptureManager::sharedCaptureManager(); + captureManager->stopCapture(); + + m_capturing = false; +} diff --git a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h index 1deddd04..49aa40b1 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h +++ b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h @@ -460,6 +460,12 @@ public: m_occlusionQuery.m_lastCommandBuffer = GetAndRetainCurrentCommandBufferIfNotCompleted(); } + // GPU capture + void CaptureFrame() + { + m_captureFrame = true; + } + private: MetalLayerHandle m_mainLayer; MetalLayerHandle m_padLayer; @@ -533,6 +539,11 @@ private: // State MetalState m_state; + // GPU capture + bool m_captureFrame = false; + bool m_capturing = false; + + // Helpers MetalLayerHandle& GetLayer(bool mainWindow) { return (mainWindow ? m_mainLayer : m_padLayer); @@ -541,4 +552,8 @@ private: void SwapBuffer(bool mainWindow); void EnsureImGuiBackend(); + + // GPU capture + void StartCapture(); + void EndCapture(); }; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 740120c1..fc5152c5 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1,3 +1,5 @@ +#include "Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h" +#include "Cafe/HW/Latte/Renderer/Renderer.h" #include "gui/wxgui.h" #include "gui/MainWindow.h" #include "gui/guiWrapper.h" @@ -137,6 +139,7 @@ enum MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY, MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, + MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, // debug->logging MAINFRAME_MENU_ID_DEBUG_LOGGING0 = 21500, @@ -212,6 +215,7 @@ EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_CURL_REQUESTS, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, MainWindow::OnDebugSetting) +EVT_MENU(MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_RAM, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_FST, MainWindow::OnDebugSetting) // debug -> View ... @@ -1007,6 +1011,20 @@ void MainWindow::OnDebugSetting(wxCommandEvent& event) if(!GetConfig().vk_accurate_barriers) wxMessageBox(_("Warning: Disabling the accurate barriers option will lead to flickering graphics but may improve performance. It is highly recommended to leave it turned on."), _("Accurate barriers are off"), wxOK); } + else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE) + { +#if ENABLE_METAL + if (g_renderer->GetType() == RendererAPI::Metal) + { + static_cast(g_renderer.get())->CaptureFrame(); + } + else + { + wxMessageBox(_("GPU capture is only supported on Metal."), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + return; + } +#endif + } else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY) ActiveSettings::EnableAudioOnlyAux(event.IsChecked()); else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_DUMP_RAM) @@ -2254,6 +2272,9 @@ void MainWindow::RecreateMenu() auto accurateBarriers = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, _("&Accurate barriers (Vulkan)"), wxEmptyString); accurateBarriers->Check(GetConfig().vk_accurate_barriers); + auto gpuCapture = debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, _("&GPU capture (Metal)"), wxEmptyString); + gpuCapture->Enable(m_game_launched && g_renderer->GetType() == RendererAPI::Metal); + debugMenu->AppendSeparator(); #ifdef CEMU_DEBUG_ASSERT From 9a61e81715245602602100200a5f6270e0a61ee2 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Sat, 4 Jan 2025 13:54:07 +0100 Subject: [PATCH 2/4] support saving GPU captures to a file --- .../HW/Latte/Renderer/Metal/MetalCommon.h | 12 +++++++ .../HW/Latte/Renderer/Metal/MetalRenderer.cpp | 35 +++++++++++++++++++ src/config/CemuConfig.cpp | 2 ++ src/config/CemuConfig.h | 1 + src/gui/GeneralSettings2.cpp | 18 ++++++++++ src/gui/GeneralSettings2.h | 1 + 6 files changed, 69 insertions(+) diff --git a/src/Cafe/HW/Latte/Renderer/Metal/MetalCommon.h b/src/Cafe/HW/Latte/Renderer/Metal/MetalCommon.h index ba9ebc36..952fd1de 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/MetalCommon.h +++ b/src/Cafe/HW/Latte/Renderer/Metal/MetalCommon.h @@ -67,6 +67,18 @@ inline NS::String* ToNSString(const std::string& str) return ToNSString(str.c_str()); } +// Cast from const char* to NS::URL* +inline NS::URL* ToNSURL(const char* str) +{ + return NS::URL::fileURLWithPath(ToNSString(str)); +} + +// Cast from std::string to NS::URL* +inline NS::URL* ToNSURL(const std::string& str) +{ + return ToNSURL(str.c_str()); +} + inline NS::String* GetLabel(const std::string& label, const void* identifier) { return ToNSString(label + " (" + std::to_string(reinterpret_cast(identifier)) + ")"); diff --git a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp index 72b756e1..19a4b55d 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp @@ -20,6 +20,9 @@ #include "Cemu/Logging/CemuLogging.h" #include "Cafe/HW/Latte/Core/FetchShader.h" #include "Cafe/HW/Latte/Core/LatteConst.h" +#include "Common/precompiled.h" +#include "HW/Latte/Renderer/Metal/MetalCommon.h" +#include "Metal/MTLCaptureManager.hpp" #include "config/CemuConfig.h" #include "gui/guiWrapper.h" @@ -2179,6 +2182,38 @@ void MetalRenderer::StartCapture() auto desc = MTL::CaptureDescriptor::alloc()->init(); desc->setCaptureObject(m_device); + // Check if a debugger with support for GPU capture is attached + if (captureManager->supportsDestination(MTL::CaptureDestinationDeveloperTools)) + { + desc->setDestination(MTL::CaptureDestinationDeveloperTools); + } + else + { + if (GetConfig().gpu_capture_dir.GetValue().empty()) + { + cemuLog_log(LogType::Force, "No GPU capture directory specified, cannot do a GPU capture"); + return; + } + + // Check if the GPU trace document destination is available + if (!captureManager->supportsDestination(MTL::CaptureDestinationGPUTraceDocument)) + { + cemuLog_log(LogType::Force, "GPU trace document destination is not available, cannot do a GPU capture"); + return; + } + + // Get current date and time as a string + auto now = std::chrono::system_clock::now(); + std::time_t now_time = std::chrono::system_clock::to_time_t(now); + std::ostringstream oss; + oss << std::put_time(std::localtime(&now_time), "%Y-%m-%d_%H-%M-%S"); + std::string now_str = oss.str(); + + std::string capturePath = fmt::format("{}/cemu_{}.gputrace", GetConfig().gpu_capture_dir.GetValue(), now_str); + desc->setDestination(MTL::CaptureDestinationGPUTraceDocument); + desc->setOutputURL(ToNSURL(capturePath)); + } + NS::Error* error = nullptr; captureManager->startCapture(desc, &error); if (error) diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 00e56d6d..cbea09cb 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -336,6 +336,7 @@ void CemuConfig::Load(XMLConfigParser& parser) crash_dump = debug.get("CrashDumpUnix", crash_dump); #endif gdb_port = debug.get("GDBPort", 1337); + gpu_capture_dir = debug.get("GPUCaptureDir", ""); // input auto input = parser.get("Input"); @@ -537,6 +538,7 @@ void CemuConfig::Save(XMLConfigParser& parser) debug.set("CrashDumpUnix", crash_dump.GetValue()); #endif debug.set("GDBPort", gdb_port); + debug.set("GPUCaptureDir", gpu_capture_dir.GetValue()); // input auto input = config.set("Input"); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index b3ff1999..08a7c994 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -526,6 +526,7 @@ struct CemuConfig // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; ConfigValue gdb_port{ 1337 }; + ConfigValue gpu_capture_dir{}; void Load(XMLConfigParser& parser); void Save(XMLConfigParser& parser); diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index 8663dbff..68cd93ed 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -892,6 +893,21 @@ wxPanel* GeneralSettings2::AddDebugPage(wxNotebook* notebook) debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); } + { + auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); + debug_row->SetFlexibleDirection(wxBOTH); + debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); + + debug_row->Add(new wxStaticText(panel, wxID_ANY, _("GPU capture save directory"), wxDefaultPosition, wxDefaultSize, 0), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + + m_gpu_capture_dir = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_DONTWRAP); + m_gpu_capture_dir->SetMinSize(wxSize(150, -1)); + m_gpu_capture_dir->SetToolTip(_("Cemu will save the GPU captures done by selecting Debug -> GPU capture in the menu bar in this directory. If a debugger with support for GPU captures (like Xcode) is attached, the capture will be opened in that debugger instead.")); + + debug_row->Add(m_gpu_capture_dir, 0, wxALL | wxEXPAND, 5); + debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); + } + panel->SetSizerAndFit(debug_panel_sizer); return panel; @@ -1101,6 +1117,7 @@ void GeneralSettings2::StoreConfig() // debug config.crash_dump = (CrashDump)m_crash_dump->GetSelection(); config.gdb_port = m_gdb_port->GetValue(); + config.gpu_capture_dir = m_gpu_capture_dir->GetValue().utf8_string(); g_config.Save(); } @@ -1794,6 +1811,7 @@ void GeneralSettings2::ApplyConfig() // debug m_crash_dump->SetSelection((int)config.crash_dump.GetValue()); m_gdb_port->SetValue(config.gdb_port.GetValue()); + m_gpu_capture_dir->SetValue(wxHelper::FromUtf8(config.gpu_capture_dir.GetValue())); } void GeneralSettings2::OnAudioAPISelected(wxCommandEvent& event) diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index 83ede03b..2551b2bd 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -78,6 +78,7 @@ private: // Debug wxChoice* m_crash_dump; wxSpinCtrl* m_gdb_port; + wxTextCtrl* m_gpu_capture_dir; void OnAccountCreate(wxCommandEvent& event); void OnAccountDelete(wxCommandEvent& event); From 813c52c23cb609d5803d5f292a441c3ef19d0f4f Mon Sep 17 00:00:00 2001 From: Samuliak Date: Sat, 4 Jan 2025 13:55:49 +0100 Subject: [PATCH 3/4] add gpu capture environment notice --- src/gui/GeneralSettings2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index 68cd93ed..9c6d8580 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -902,7 +902,7 @@ wxPanel* GeneralSettings2::AddDebugPage(wxNotebook* notebook) m_gpu_capture_dir = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_DONTWRAP); m_gpu_capture_dir->SetMinSize(wxSize(150, -1)); - m_gpu_capture_dir->SetToolTip(_("Cemu will save the GPU captures done by selecting Debug -> GPU capture in the menu bar in this directory. If a debugger with support for GPU captures (like Xcode) is attached, the capture will be opened in that debugger instead.")); + m_gpu_capture_dir->SetToolTip(_("Cemu will save the GPU captures done by selecting Debug -> GPU capture in the menu bar in this directory. If a debugger with support for GPU captures (like Xcode) is attached, the capture will be opened in that debugger instead. If such debugger is not attached, METAL_CAPTURE_ENABLED must be set to 1 as an environment variable.")); debug_row->Add(m_gpu_capture_dir, 0, wxALL | wxEXPAND, 5); debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); From 337ec6b721accd855e286aa2134be932e6e12e72 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Sat, 4 Jan 2025 17:02:03 +0100 Subject: [PATCH 4/4] fix: GPU capture button not working --- src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp | 3 +++ src/gui/MainWindow.cpp | 14 ++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp index 19a4b55d..c5bdd335 100644 --- a/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Metal/MetalRenderer.cpp @@ -188,6 +188,9 @@ MetalRenderer::MetalRenderer() m_copyBufferToBufferPipeline = new MetalVoidVertexPipeline(this, utilityLibrary, "vertexCopyBufferToBuffer"); utilityLibrary->release(); + + // HACK: for some reason, this variable ends up being initialized to some garbage data, even though its declared as bool m_captureFrame = false; + m_captureFrame = false; } MetalRenderer::~MetalRenderer() diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index fc5152c5..e7cccca8 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1013,16 +1013,10 @@ void MainWindow::OnDebugSetting(wxCommandEvent& event) } else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE) { + cemu_assert_debug(g_renderer->GetType() == RendererAPI::Metal); + #if ENABLE_METAL - if (g_renderer->GetType() == RendererAPI::Metal) - { - static_cast(g_renderer.get())->CaptureFrame(); - } - else - { - wxMessageBox(_("GPU capture is only supported on Metal."), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return; - } + static_cast(g_renderer.get())->CaptureFrame(); #endif } else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY) @@ -2272,7 +2266,7 @@ void MainWindow::RecreateMenu() auto accurateBarriers = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, _("&Accurate barriers (Vulkan)"), wxEmptyString); accurateBarriers->Check(GetConfig().vk_accurate_barriers); - auto gpuCapture = debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, _("&GPU capture (Metal)"), wxEmptyString); + auto gpuCapture = debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, _("&GPU capture (Metal)")); gpuCapture->Enable(m_game_launched && g_renderer->GetType() == RendererAPI::Metal); debugMenu->AppendSeparator();