From bf14dd4e5cf7cea786002bde0eb858fc7cdb1692 Mon Sep 17 00:00:00 2001 From: ParticleTroned <248299730+ParticleTroned@users.noreply.github.com> Date: Wed, 6 May 2026 07:44:22 +0100 Subject: [PATCH 1/2] feat(renderdoc): add multi-frame captures --- src/Features/RenderDoc.cpp | 141 ++++++++++++++++++++++++++++++++++--- src/Features/RenderDoc.h | 11 +++ src/Menu.cpp | 11 +-- 3 files changed, 148 insertions(+), 15 deletions(-) diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp index b7c65876c5..7c32085fee 100644 --- a/src/Features/RenderDoc.cpp +++ b/src/Features/RenderDoc.cpp @@ -117,6 +117,8 @@ void RenderDoc::Load() } renderDocApi->MaskOverlayBits(eRENDERDOC_Overlay_None, eRENDERDOC_Overlay_None); + // Menu input owns capture hotkeys so they respect the configured frame count. + renderDocApi->SetCaptureKeys(nullptr, 0); // Initialize capture count tracking lastCaptureCount = renderDocApi->GetNumCaptures(); @@ -188,18 +190,16 @@ void RenderDoc::DrawSettings() ImGui::InputTextWithHint("##CaptureComments", "Additional comments for next capture (optional)", commentsBuffer, sizeof(commentsBuffer)); Util::AddTooltip("Additional comments will be appended to automatic metadata and embedded in the .rdc file"); + int captureFrameCountUI = static_cast(GetCaptureFrameCount()); + if (ImGui::SliderInt("Capture Frames", &captureFrameCountUI, static_cast(kMinCaptureFrameCount), static_cast(kMaxCaptureFrameCount), "%d", ImGuiSliderFlags_AlwaysClamp)) { + SetCaptureFrameCount(static_cast(captureFrameCountUI)); + } + Util::AddTooltip("Number of consecutive frames to capture. 1 uses a normal RenderDoc capture; higher values use TriggerMultiFrameCapture."); + if (ImGui::Button("Create Capture")) { // Check available disk space before allowing capture try { - auto capturesDir = GetCapturesDirectory(); - std::error_code ec; - uint64_t freeSpace = std::filesystem::space(capturesDir, ec).available; - if (ec) { - logger::warn("[RenderDoc] Failed to check available space in '{}': {}", capturesDir, ec.message()); - freeSpace = 0; - } - - if (freeSpace < kMinCaptureSpaceBytes) { + if (!HasSufficientDiskSpaceForConfiguredCapture()) { ImGui::OpenPopup("Not enough disk space##RenderDoc"); } else { // Set comments if provided @@ -215,7 +215,7 @@ void RenderDoc::DrawSettings() // Actual capture logic logger::info("[RenderDoc] Manual capture triggered by user"); - TriggerCapture(); + TriggerConfiguredCapture(false); } } catch (const std::exception& e) { logger::error("[RenderDoc] Exception during capture logic: {}", e.what()); @@ -224,7 +224,7 @@ void RenderDoc::DrawSettings() if (ImGui::BeginPopup("Not enough disk space##RenderDoc")) { ImGui::Text("Not enough free disk space to create a capture."); - ImGui::Text("At least {} MB of free space is required.", kMinCaptureSpaceBytes / (1024 * 1024)); + ImGui::Text("At least {} MB of free space is required.", GetRequiredCaptureSpaceBytes() / (1024 * 1024)); if (ImGui::Button("OK")) { ImGui::CloseCurrentPopup(); } @@ -540,6 +540,7 @@ void RenderDoc::SetupResources() void RenderDoc::SaveSettings(json& o_json) { o_json["Enable RenderDoc Capture"] = enableRenderDocCapture; + o_json["Capture Frame Count"] = GetCaptureFrameCount(); } void RenderDoc::LoadSettings(json& o_json) @@ -547,11 +548,27 @@ void RenderDoc::LoadSettings(json& o_json) if (o_json.contains("Enable RenderDoc Capture") && o_json["Enable RenderDoc Capture"].is_boolean()) { enableRenderDocCapture = o_json["Enable RenderDoc Capture"]; } + if (!o_json.contains("Capture Frame Count")) { + return; + } + + const auto& frameCountJson = o_json["Capture Frame Count"]; + if (frameCountJson.is_number_unsigned()) { + const auto frameCount = std::min(frameCountJson.get(), static_cast(kMaxCaptureFrameCount)); + SetCaptureFrameCount(static_cast(frameCount)); + } else if (frameCountJson.is_number_integer()) { + const auto frameCount = std::clamp( + frameCountJson.get(), + static_cast(kMinCaptureFrameCount), + static_cast(kMaxCaptureFrameCount)); + SetCaptureFrameCount(static_cast(frameCount)); + } } void RenderDoc::RestoreDefaultSettings() { enableRenderDocCapture = false; + SetCaptureFrameCount(1); } void RenderDoc::ClearShaderCache() @@ -643,6 +660,108 @@ void RenderDoc::TriggerCapture() InvalidateCaptureCache(); } +void RenderDoc::TriggerMultiFrameCapture(uint32_t a_frameCount) +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot trigger multi-frame capture - RenderDoc API not available"); + return; + } + + const uint32_t frameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); + logger::info("[RenderDoc] Triggering multi-frame capture for {} frame(s)", frameCount); + renderDocApi->TriggerMultiFrameCapture(frameCount); + + // Invalidate cache so it refreshes when next accessed (capture should appear in list) + InvalidateCaptureCache(); +} + +bool RenderDoc::TriggerConfiguredCapture(bool a_checkDiskSpace) +{ + if (!renderDocApi) { + logger::warn("[RenderDoc] Cannot trigger configured capture - RenderDoc API not available"); + return false; + } + + if (a_checkDiskSpace) { + uint64_t requiredSpace = 0; + if (!HasSufficientDiskSpaceForConfiguredCapture(&requiredSpace)) { + logger::warn("[RenderDoc] Not enough free disk space to create a capture. At least {} MB required.", requiredSpace / (1024 * 1024)); + return false; + } + } + + const uint32_t frameCount = GetCaptureFrameCount(); + if (frameCount > 1) { + TriggerMultiFrameCapture(frameCount); + } else { + TriggerCapture(); + } + + return true; +} + +bool RenderDoc::HandleCaptureHotkey(uint32_t a_vkKey) +{ + if (a_vkKey != VK_F12 && a_vkKey != VK_SNAPSHOT) { + return false; + } + + constexpr int kKeyPressedMask = 0x8000; + const bool modifierHeld = + (GetAsyncKeyState(VK_CONTROL) & kKeyPressedMask) != 0 || + (GetAsyncKeyState(VK_SHIFT) & kKeyPressedMask) != 0 || + (GetAsyncKeyState(VK_MENU) & kKeyPressedMask) != 0; + if (modifierHeld) { + return false; + } + + if (!IsAvailable()) { + return false; + } + + logger::info("[RenderDoc] Capture hotkey triggered"); + TriggerConfiguredCapture(); + return true; +} + +uint32_t RenderDoc::GetCaptureFrameCount() const +{ + return std::clamp(captureFrameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); +} + +void RenderDoc::SetCaptureFrameCount(uint32_t a_frameCount) +{ + captureFrameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount); +} + +uint64_t RenderDoc::GetRequiredCaptureSpaceBytes() const +{ + return kMinCaptureSpaceBytes * GetCaptureFrameCount(); +} + +bool RenderDoc::HasSufficientDiskSpaceForConfiguredCapture(uint64_t* a_requiredSpaceBytes) const +{ + const uint64_t requiredSpace = GetRequiredCaptureSpaceBytes(); + if (a_requiredSpaceBytes) { + *a_requiredSpaceBytes = requiredSpace; + } + + try { + auto capturesDir = GetCapturesDirectory(); + std::error_code ec; + const uint64_t freeSpace = std::filesystem::space(capturesDir, ec).available; + if (ec) { + logger::warn("[RenderDoc] Failed to check available space in '{}': {}", capturesDir, ec.message()); + return false; + } + + return freeSpace >= requiredSpace; + } catch (const std::exception& e) { + logger::error("[RenderDoc] Exception while checking capture disk space: {}", e.what()); + return false; + } +} + void RenderDoc::SetCaptureFilePathTemplate(const std::string& a_template) { if (!renderDocApi) { diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index e0e0e8d9cd..ba3ef60a89 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -36,6 +36,8 @@ class RenderDoc : public Feature // Core RenderDoc functionality bool IsAvailable() const { return renderDocApi != nullptr; } void TriggerCapture(); + void TriggerMultiFrameCapture(uint32_t a_frameCount); + bool HandleCaptureHotkey(uint32_t a_vkKey); void SetCaptureFilePathTemplate(const std::string& a_template); std::string GetCapturesDirectory() const; bool IsCapturing() const; @@ -87,6 +89,12 @@ class RenderDoc : public Feature std::string GetOverlayWarningMessage() const; private: + bool TriggerConfiguredCapture(bool a_checkDiskSpace = true); + uint32_t GetCaptureFrameCount() const; + void SetCaptureFrameCount(uint32_t a_frameCount); + uint64_t GetRequiredCaptureSpaceBytes() const; + bool HasSufficientDiskSpaceForConfiguredCapture(uint64_t* a_requiredSpaceBytes = nullptr) const; + // Cache management for capture files void RefreshCaptureFileCache(); const std::vector& GetCachedCaptureFiles(); @@ -112,6 +120,7 @@ class RenderDoc : public Feature // RenderDoc capture enable setting bool enableRenderDocCapture = false; + uint32_t captureFrameCount = 1; // Track the last capture count we've processed for automatic comments uint32_t lastCaptureCount = 0; @@ -128,4 +137,6 @@ class RenderDoc : public Feature static constexpr uint64_t kMinCaptureSpaceBytes = 100ULL * 1024ULL * 1024ULL; // 100 MB minimum free space static constexpr uint32_t kCacheRefreshIntervalSeconds = 5; // Cache refresh interval static constexpr size_t kCommentsBufferSize = 1024; // Size of comments input buffer + static constexpr uint32_t kMinCaptureFrameCount = 1; + static constexpr uint32_t kMaxCaptureFrameCount = 120; }; diff --git a/src/Menu.cpp b/src/Menu.cpp index 2b5d6e1b5a..ed884db272 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -22,6 +22,7 @@ #include "Feature.h" #include "FeatureIssues.h" #include "FeatureVersions.h" +#include "Features/RenderDoc.h" #include "Features/Upscaling.h" #include "Menu/AdvancedSettingsRenderer.h" #include "Menu/BackgroundBlur.h" @@ -1072,10 +1073,12 @@ void Menu::ProcessInputEventQueue() globals::features::screenshotFeature.captureRequested = true; } }, }; - for (const auto& ka : keyActions) { - if (InputCombo::MatchesKeyboardCombo(ka.settingKey, key)) { - ka.action(); - break; + if (!RenderDoc::GetSingleton()->HandleCaptureHotkey(key)) { + for (const auto& ka : keyActions) { + if (InputCombo::MatchesKeyboardCombo(ka.settingKey, key)) { + ka.action(); + break; + } } } } From c6e1eed26700c70398d236a55e2505b50e69f42f Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 17 May 2026 21:46:42 -0700 Subject: [PATCH 2/2] fix(renderdoc): address PR review feedback - Replace optimistic 100MB/frame disk space estimate with a conservative 256MB/frame observation; preserve the 100MB minimum floor so single-frame captures keep their old guard. - Route the F12/SNAPSHOT hotkey dispatch through the globals::features::renderDoc registry instead of the singleton, matching the project convention. - Bump RenderDoc feature version 1-0-1 -> 1-1-0 for the new multi-frame capture setting. Co-Authored-By: Claude Opus 4.7 --- features/RenderDoc/Shaders/Features/RenderDoc.ini | 2 +- src/Features/RenderDoc.cpp | 3 ++- src/Features/RenderDoc.h | 7 ++++--- src/Menu.cpp | 14 +++++++------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/features/RenderDoc/Shaders/Features/RenderDoc.ini b/features/RenderDoc/Shaders/Features/RenderDoc.ini index f2aab7e619..0bc0292971 100644 --- a/features/RenderDoc/Shaders/Features/RenderDoc.ini +++ b/features/RenderDoc/Shaders/Features/RenderDoc.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-0-1 +Version = 1-1-0 [Nexus] autoupload = false diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp index 7c32085fee..08661b33b2 100644 --- a/src/Features/RenderDoc.cpp +++ b/src/Features/RenderDoc.cpp @@ -736,7 +736,8 @@ void RenderDoc::SetCaptureFrameCount(uint32_t a_frameCount) uint64_t RenderDoc::GetRequiredCaptureSpaceBytes() const { - return kMinCaptureSpaceBytes * GetCaptureFrameCount(); + const uint64_t estimated = kObservedPerFrameBytes * GetCaptureFrameCount(); + return std::max(estimated, kMinCaptureSpaceBytes); } bool RenderDoc::HasSufficientDiskSpaceForConfiguredCapture(uint64_t* a_requiredSpaceBytes) const diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index ba3ef60a89..ed3c133392 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -134,9 +134,10 @@ class RenderDoc : public Feature // Track files that failed to delete for UI feedback std::unordered_map failedDeletions; - static constexpr uint64_t kMinCaptureSpaceBytes = 100ULL * 1024ULL * 1024ULL; // 100 MB minimum free space - static constexpr uint32_t kCacheRefreshIntervalSeconds = 5; // Cache refresh interval - static constexpr size_t kCommentsBufferSize = 1024; // Size of comments input buffer + static constexpr uint64_t kMinCaptureSpaceBytes = 100ULL * 1024ULL * 1024ULL; // 100 MB minimum free space floor + static constexpr uint64_t kObservedPerFrameBytes = 256ULL * 1024ULL * 1024ULL; // ~256 MB per frame observed for multi-frame captures + static constexpr uint32_t kCacheRefreshIntervalSeconds = 5; // Cache refresh interval + static constexpr size_t kCommentsBufferSize = 1024; // Size of comments input buffer static constexpr uint32_t kMinCaptureFrameCount = 1; static constexpr uint32_t kMaxCaptureFrameCount = 120; }; diff --git a/src/Menu.cpp b/src/Menu.cpp index ed884db272..cb6b9c253e 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -895,7 +895,7 @@ static std::vector DeriveWeatherEditorKey(const std::vector keys) { - settings.ToggleKey = keys; - settingToggleKey = false; - if (!settings.FirstTimeSetupCompleted) - settings.WeatherEditorToggleKey = DeriveWeatherEditorKey(keys); - } }, + settings.ToggleKey = keys; + settingToggleKey = false; + if (!settings.FirstTimeSetupCompleted) + settings.WeatherEditorToggleKey = DeriveWeatherEditorKey(keys); + } }, { &settings.SkipCompilationKey, &settingSkipCompilationKey, [this](std::vector keys) { settings.SkipCompilationKey = keys; settingSkipCompilationKey = false; } }, { &settings.EffectToggleKey, &settingsEffectsToggle, [this](std::vector keys) { settings.EffectToggleKey = keys; settingsEffectsToggle = false; } }, { &settings.OverlayToggleKey, &settingOverlayToggleKey, [this](std::vector keys) { settings.OverlayToggleKey = keys; settingOverlayToggleKey = false; } }, @@ -1073,7 +1073,7 @@ void Menu::ProcessInputEventQueue() globals::features::screenshotFeature.captureRequested = true; } }, }; - if (!RenderDoc::GetSingleton()->HandleCaptureHotkey(key)) { + if (!globals::features::renderDoc.HandleCaptureHotkey(key)) { for (const auto& ka : keyActions) { if (InputCombo::MatchesKeyboardCombo(ka.settingKey, key)) { ka.action();