diff --git a/src/DX12SwapChain.cpp b/src/DX12SwapChain.cpp index 65045f2acd..3bdc7100e8 100644 --- a/src/DX12SwapChain.cpp +++ b/src/DX12SwapChain.cpp @@ -176,6 +176,29 @@ HRESULT DX12SwapChain::GetDevice(REFIID uuid, void** ppDevice) return swapChain->GetDevice(uuid, ppDevice); } +float DX12SwapChain::GetFrameTime() const +{ + // Calculate frame time based on swap chain presentation + static float lastPresentTime = 0.0f; + static float frameTime = 1.0f / 60.0f; // Default to 60 fps + static LARGE_INTEGER frequency = {}; + static LARGE_INTEGER currentTime = {}; + + if (frequency.QuadPart == 0) { + QueryPerformanceFrequency(&frequency); + } + + QueryPerformanceCounter(¤tTime); + float time = static_cast(currentTime.QuadPart) / static_cast(frequency.QuadPart); + + if (lastPresentTime > 0.0f) { + frameTime = time - lastPresentTime; + } + lastPresentTime = time; + + return frameTime; +} + WrappedResource::WrappedResource(D3D11_TEXTURE2D_DESC a_texDesc, ID3D11Device5* a_d3d11Device, ID3D12Device* a_d3d12Device) { D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS; diff --git a/src/DX12SwapChain.h b/src/DX12SwapChain.h index efcff3c98a..5fbf427bb3 100644 --- a/src/DX12SwapChain.h +++ b/src/DX12SwapChain.h @@ -94,6 +94,9 @@ class DX12SwapChain DXGISwapChainProxy* swapChainProxy = nullptr; + // Returns the current frame time (in seconds) for accurate FPS calculation when frame generation is active + float GetFrameTime() const; + void CreateD3D12Device(IDXGIAdapter* a_adapter); void CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC swapChainDesc); diff --git a/src/Menu.cpp b/src/Menu.cpp index 9c4d2c250d..9158b606f6 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -32,6 +32,26 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Error, CurrentHotkey) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::Settings::PerfOverlaySettings, + Enabled, + ShowDrawCalls, + ShowVRAM, + ShowFPS, + ShowPreFGFrameTime, + ShowPreFGFrameTimeGraph, + ShowPreFGFPS, + ShowPostFGFPS, + ShowPostFGFrameTime, + ShowPostFGFrameTimeGraph, + UpdateInterval, + Size, + BackgroundOpacity, + ShowBorder, + Position, + PositionSet, + OverlayToggleKey) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ImGuiStyle, WindowPadding, @@ -82,7 +102,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ToggleKey, SkipCompilationKey, EffectToggleKey, - Theme) + Theme, + PerfOverlay) void Menu::SetupImGuiStyle() const { @@ -211,9 +232,9 @@ void Menu::Init() imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset; ImFontConfig font_config; - font_config.GlyphExtraSpacing.x = -0.5; + font_config.GlyphExtraSpacing.x = -0.5f; - imgui_io.Fonts->AddFontFromFileTTF("Data\\Interface\\CommunityShaders\\Fonts\\Jost-Regular.ttf", 36, &font_config); + imgui_io.Fonts->AddFontFromFileTTF("Data\\Interface\\CommunityShaders\\Fonts\\Jost-Regular.ttf", 36.0f, &font_config); DXGI_SWAP_CHAIN_DESC desc; globals::d3d::swapChain->GetDesc(&desc); @@ -252,7 +273,7 @@ void Menu::DrawSettings() if (!ImGui::IsWindowDocked()) { ImGui::SetWindowFontScale(1.5f); ImGui::TextUnformatted(title.c_str()); - ImGui::SetWindowFontScale(1.f); + ImGui::SetWindowFontScale(1.0f); ImGui::Spacing(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3.0f); @@ -588,7 +609,6 @@ void Menu::DrawGeneralSettings() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Disabling this stops shaders from being loaded from disk, as well as stops shaders from being saved to it."); } - bool useAsync = shaderCache->IsAsync(); ImGui::TableNextColumn(); if (ImGui::Checkbox("Enable Async", &useAsync)) { @@ -656,7 +676,7 @@ void Menu::DrawGeneralSettings() if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { if (ImGui::BeginTabItem("Sizes")) { - if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.f, 1.f, "%.2f")) { + if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.0f, 1.0f, "%.2f")) { float trueScale = exp2(themeSettings.GlobalScale); auto& io = ImGui::GetIO(); @@ -977,7 +997,8 @@ void Menu::DrawDisplaySettings() auto& themeSettings = settings.Theme; const std::vector>> features = { - { "Upscaling", []() { globals::upscaling->DrawSettings(); } } + { "Upscaling", []() { globals::upscaling->DrawSettings(); } }, + { "Performance Overlay", [this]() { DrawPerformanceOverlaySettings(); } } }; for (const auto& [featureName, drawFunc] : features) { @@ -1014,20 +1035,7 @@ void Menu::DrawFooter() ImGui::SameLine(); ImGui::BulletText(std::format("D3D12 Interop: {}", globals::upscaling->d3d12Interop ? "Active" : "Inactive").c_str()); ImGui::SameLine(); - ImGui::BulletText(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); - - if (dxgiAdapter3) { - ImGui::SameLine(); - DXGI_QUERY_VIDEO_MEMORY_INFO videoMemoryInfo; - dxgiAdapter3->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &videoMemoryInfo); - - float currentGpuUsage = videoMemoryInfo.CurrentUsage / (1024.f * 1024.f * 1024.f); - float totalGpuMemory = videoMemoryInfo.Budget / (1024.f * 1024.f * 1024.f); - float percent = currentGpuUsage / totalGpuMemory; - - auto progressOverlay = std::format("GPU Usage: {:.02f}GB/{:.02f}GB ({:2.1f}%) ", currentGpuUsage, totalGpuMemory, 100 * percent); - ImGui::ProgressBar(percent, ImVec2(500.f, ImGui::GetTextLineHeight()), progressOverlay.c_str()); - } + ImGui::Text(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); } void Menu::DrawOverlay() @@ -1037,8 +1045,7 @@ void Menu::DrawOverlay() auto shaderCache = globals::shaderCache; auto failed = shaderCache->GetFailedTasks(); auto hide = shaderCache->IsHideErrors(); - - if (!(shaderCache->IsCompiling() || IsEnabled || inTestMode || (failed && !hide))) { + if (!(shaderCache->IsCompiling() || IsEnabled || inTestMode || (failed && !hide) || settings.PerfOverlay.Enabled)) { auto& io = ImGui::GetIO(); io.ClearInputKeys(); io.ClearEventsQueue(); @@ -1103,17 +1110,19 @@ void Menu::DrawOverlay() } else { ImGui::GetIO().MouseDrawCursor = false; } + if (settings.PerfOverlay.Enabled) + DrawPerfOverlay(); if (inTestMode) { // In test mode - float seconds = (float)duration_cast(high_resolution_clock::now() - lastTestSwitch).count() / 1000; + float seconds = (float)duration_cast(high_resolution_clock::now() - lastTestSwitch).count() / 1000.0f; auto remaining = (float)testInterval - seconds; - if (remaining < 0) { + if (remaining < 0.0f) { usingTestConfig = !usingTestConfig; logger::info("Swapping mode to {}", usingTestConfig ? "test" : "user"); globals::state->Load(usingTestConfig ? State::ConfigMode::TEST : State::ConfigMode::USER); lastTestSwitch = high_resolution_clock::now(); } - ImGui::SetNextWindowBgAlpha(1); + ImGui::SetNextWindowBgAlpha(1.0f); ImGui::SetNextWindowPos(ImVec2(10, 10)); if (!ImGui::Begin("Testing", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { ImGui::End(); @@ -1130,6 +1139,485 @@ void Menu::DrawOverlay() ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); } +void Menu::DrawPerfOverlay() +{ + if (!settings.PerfOverlay.Enabled) { + return; + } + + // Set window flags - no decoration and only movable when ShowBorder is true + ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize; + + // Only allow mouse interaction when the main menu is open + if (!IsEnabled) { + windowFlags |= ImGuiWindowFlags_NoInputs; + } + + if (!settings.PerfOverlay.ShowBorder) { + windowFlags |= ImGuiWindowFlags_NoBackground; + } else { + windowFlags &= ~ImGuiWindowFlags_NoDecoration; + windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + } + + // Set background opacity + ImGui::PushStyleColor(ImGuiCol_WindowBg, + ImVec4(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).x, + ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).y, + ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).z, + settings.PerfOverlay.BackgroundOpacity)); + + // Set text size based on user preference + float textScale = 1.0f; + switch (settings.PerfOverlay.Size) { + case Settings::PerfOverlaySettings::TextSize::Small: + textScale = 0.8f; + break; + case Settings::PerfOverlaySettings::TextSize::Medium: + textScale = 1.0f; + break; + case Settings::PerfOverlaySettings::TextSize::Large: + textScale = 1.2f; + break; + } + + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, settings.PerfOverlay.ShowBorder ? 1.0f : 0.0f); + + // Set initial position if not already set + if (!settings.PerfOverlay.PositionSet) { + ImGui::SetNextWindowPos(ImVec2(10.0f, 10.0f)); + settings.PerfOverlay.Position = ImVec2(10.0f, 10.0f); + settings.PerfOverlay.PositionSet = true; + } else { + ImGui::SetNextWindowPos(settings.PerfOverlay.Position, ImGuiCond_FirstUseEver); + } + + // Create the window + ImGui::Begin("Performance Overlay", NULL, windowFlags); + + // Remember window position for next frame + if (ImGui::IsWindowAppearing()) { + ImGui::SetWindowPos(settings.PerfOverlay.Position); + } + + // Track if window has been moved + ImVec2 currentPos = ImGui::GetWindowPos(); + if (currentPos.x != settings.PerfOverlay.Position.x || currentPos.y != settings.PerfOverlay.Position.y) { + settings.PerfOverlay.Position = currentPos; + } + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 1.0f)); // Tighter spacing + ImGui::SetWindowFontScale(textScale); + + // Get frame timing data using QueryPerformanceCounter for precise measurements + static LARGE_INTEGER frequency; + static LARGE_INTEGER lastFrameCounter; + static LARGE_INTEGER currentFrameCounter; + static float frameTimeMs = 0.0f; + static float fps = 0.0f; + static float smoothFps = 0.0f; + static float smoothFrameTimeMs = 0.0f; + static float postFGSmoothFps = 0.0f; + static float postFGSmoothFrameTimeMs = 0.0f; + static float updateTimer = 0.0f; + static std::chrono::steady_clock::time_point lastUpdateTime = std::chrono::steady_clock::now(); + + if (frequency.QuadPart == 0) { + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&lastFrameCounter); + } + + QueryPerformanceCounter(¤tFrameCounter); + LONGLONG elapsedCounter = currentFrameCounter.QuadPart - lastFrameCounter.QuadPart; + lastFrameCounter = currentFrameCounter; + + // Calculate frametime in milliseconds + frameTimeMs = 1000.0f * (float)elapsedCounter / (float)frequency.QuadPart; + + // Calculate FPS directly from frametime + fps = 1000.0f / frameTimeMs; + + // Calculate smooth values for display using the user-defined update interval + auto currentTime = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration(currentTime - lastUpdateTime).count(); + lastUpdateTime = currentTime; + + // Frametime history graph data + const int FRAME_HISTORY_SIZE = 120; + static float frameTimeHistory[FRAME_HISTORY_SIZE] = {}; + static int frameTimeHistoryIndex = 0; + static float postFGFrameTimeHistory[FRAME_HISTORY_SIZE] = {}; + static int postFGFrameTimeHistoryIndex = 0; + + // Update frametime history + frameTimeHistory[frameTimeHistoryIndex] = frameTimeMs; + frameTimeHistoryIndex = (frameTimeHistoryIndex + 1) % FRAME_HISTORY_SIZE; + + // Update smooth values with user-specified interval + updateTimer += deltaTime; + if (updateTimer >= settings.PerfOverlay.UpdateInterval) { + smoothFps = fps; + smoothFrameTimeMs = frameTimeMs; + updateTimer = 0.0f; + } + + // Check if Frame Generation is active + bool isFrameGenerationActive = globals::upscaling && globals::upscaling->IsFrameGenerationActive(); + float postFGFrameTimeMs = 0.0f; + float postFGFps = 0.0f; + + if (isFrameGenerationActive) { + // Get frametime directly from the Frame Generation system + float fgDeltaTime = globals::upscaling->GetFrameGenerationFrameTime(); + if (fgDeltaTime > 0.0f) { + postFGFrameTimeMs = fgDeltaTime * 1000.0f; + postFGFps = 1000.0f / postFGFrameTimeMs; + + // Update post-FG smooth values when timer elapses + if (updateTimer == 0.0f) { + postFGSmoothFps = postFGFps; + postFGSmoothFrameTimeMs = postFGFrameTimeMs; + } + + // Update post-FG frametime history + postFGFrameTimeHistory[postFGFrameTimeHistoryIndex] = postFGFrameTimeMs; + postFGFrameTimeHistoryIndex = (postFGFrameTimeHistoryIndex + 1) % FRAME_HISTORY_SIZE; + } else { + // Fallback if FG time is not available + postFGFrameTimeMs = frameTimeMs / 2.0f; // Approximate + postFGFps = fps * 2.0f; // Approximate + + // Update smooth values when timer elapses + if (updateTimer == 0.0f) { + postFGSmoothFps = postFGFps; + postFGSmoothFrameTimeMs = postFGFrameTimeMs; + } + + // Update post-FG frametime history with approximation + postFGFrameTimeHistory[postFGFrameTimeHistoryIndex] = postFGFrameTimeMs; + postFGFrameTimeHistoryIndex = (postFGFrameTimeHistoryIndex + 1) % FRAME_HISTORY_SIZE; + } + } + + // Show FPS counter if enabled + if (settings.PerfOverlay.ShowFPS) { + if (isFrameGenerationActive) { + if (settings.PerfOverlay.ShowPostFGFPS) { + ImGui::Text("FPS: %.1f", postFGSmoothFps); + } + + if (settings.PerfOverlay.ShowPreFGFPS) { + ImGui::Text("Pre-FG FPS: %.1f", smoothFps); + } + } else { + ImGui::Text("FPS: %.1f", smoothFps); + } + + if (isFrameGenerationActive) { + if (settings.PerfOverlay.ShowPostFGFPS && settings.PerfOverlay.ShowPostFGFrameTime) { + ImGui::Text("Frametime: %.2f ms", postFGSmoothFrameTimeMs); + } + if (settings.PerfOverlay.ShowPreFGFPS && settings.PerfOverlay.ShowPreFGFrameTime) { + ImGui::Text("Pre-FG Frametime: %.2f ms", smoothFrameTimeMs); + } + } else { + if (settings.PerfOverlay.ShowPreFGFrameTime) { + ImGui::Text("Frametime: %.2f ms", smoothFrameTimeMs); + } + } + + // Show Pre-FG frametime graph if enabled + if (settings.PerfOverlay.ShowPreFGFrameTimeGraph && + ((isFrameGenerationActive && settings.PerfOverlay.ShowPreFGFPS) || !isFrameGenerationActive)) { + // Find min and max values for better scaling + float minFrameTime = 1000.0f; + float maxFrameTime = 0.0f; + + for (int i = 0; i < FRAME_HISTORY_SIZE; i++) { + if (frameTimeHistory[i] > 0.01f) { // Ignore empty values + minFrameTime = std::min(minFrameTime, frameTimeHistory[i]); + maxFrameTime = std::max(maxFrameTime, frameTimeHistory[i]); + } + } + + // Add some padding to min/max + minFrameTime = std::max(0.0f, minFrameTime - 1.0f); + maxFrameTime = maxFrameTime + 1.0f; + + // Prepare overlay text + char overlay_text[128]; + snprintf(overlay_text, IM_ARRAYSIZE(overlay_text), + "%s%.2f ms (%.1f FPS)", + isFrameGenerationActive ? "Pre-FG: " : "", + smoothFrameTimeMs, smoothFps); + + // Set graph colors + ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f)); // Green line + + // Draw the graph + ImGui::PlotLines("##frametime", + frameTimeHistory, + FRAME_HISTORY_SIZE, + frameTimeHistoryIndex, + overlay_text, + minFrameTime, maxFrameTime, + ImVec2(ImGui::GetWindowWidth() * 0.9f, 50.0f * textScale)); + + ImGui::PopStyleColor(); + + // Draw frametime target reference lines + if (ImGui::BeginTable("FrametimeTargets", 3, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextColumn(); + ImGui::Text("30 FPS: 33.3 ms"); + + ImGui::TableNextColumn(); + ImGui::Text("60 FPS: 16.7 ms"); + + ImGui::TableNextColumn(); + ImGui::Text("120 FPS: 8.3 ms"); + + ImGui::EndTable(); + } + } + + // Show Post-FG frametime graph if enabled + if (settings.PerfOverlay.ShowPostFGFrameTimeGraph && isFrameGenerationActive && settings.PerfOverlay.ShowPostFGFPS) { + // Find min and max values for better scaling + float minFrameTime = 1000.0f; + float maxFrameTime = 0.0f; + + for (int i = 0; i < FRAME_HISTORY_SIZE; i++) { + if (postFGFrameTimeHistory[i] > 0.01f) { // Ignore empty values + minFrameTime = std::min(minFrameTime, postFGFrameTimeHistory[i]); + maxFrameTime = std::max(maxFrameTime, postFGFrameTimeHistory[i]); + } + } + + // Add some padding to min/max + minFrameTime = std::max(0.0f, minFrameTime - 1.0f); + maxFrameTime = maxFrameTime + 1.0f; + + // Prepare overlay text + char overlay_text[128]; + snprintf(overlay_text, IM_ARRAYSIZE(overlay_text), + "Post-FG: %.2f ms (%.1f FPS)", + postFGSmoothFrameTimeMs, postFGSmoothFps); + + // Set graph colors - blue for post-FG + ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 0.5f, 1.0f, 1.0f)); // Blue line + + // Draw the graph + ImGui::PlotLines("##postfgframetime", + postFGFrameTimeHistory, + FRAME_HISTORY_SIZE, + postFGFrameTimeHistoryIndex, + overlay_text, + minFrameTime, maxFrameTime, + ImVec2(ImGui::GetWindowWidth() * 0.9f, 50.0f * textScale)); + + ImGui::PopStyleColor(); + + // Draw frametime target reference lines + if (ImGui::BeginTable("PostFGFrametimeTargets", 3, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextColumn(); + ImGui::Text("30 FPS: 33.3 ms"); + + ImGui::TableNextColumn(); + ImGui::Text("60 FPS: 16.7 ms"); + + ImGui::TableNextColumn(); + ImGui::Text("120 FPS: 8.3 ms"); + + ImGui::EndTable(); + } + } + } + + // Show Draw Calls if enabled + if (settings.PerfOverlay.ShowDrawCalls) { + ImGui::Text("Draw Calls:"); + ImGui::Indent(); + ImGui::Text("Grass: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Grass])); + ImGui::Text("Sky: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Sky])); + ImGui::Text("Water: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Water])); + ImGui::Text("Lighting: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Lighting])); + ImGui::Text("Effect: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Effect])); + ImGui::Text("Utility: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Utility])); + ImGui::Text("Distant Tree: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::DistantTree])); + ImGui::Text("Particle: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Particle])); + ImGui::Text("Total: %d", int(globals::state->smoothDrawCalls[RE::BSShader::Type::Total])); + ImGui::Unindent(); + } + + // VRAM & GPU Usage + if (settings.PerfOverlay.ShowVRAM && dxgiAdapter3) { + DXGI_QUERY_VIDEO_MEMORY_INFO videoMemoryInfo{}; + HRESULT hr = dxgiAdapter3->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &videoMemoryInfo); + + // Only proceed if the call succeeded and Budget is not zero + if (SUCCEEDED(hr) && videoMemoryInfo.Budget > 0) { + float currentGpuUsage = videoMemoryInfo.CurrentUsage / (1024.f * 1024.f * 1024.f); + float totalGpuMemory = videoMemoryInfo.Budget / (1024.f * 1024.f * 1024.f); + float percent = currentGpuUsage / totalGpuMemory; + + // Center the VRAM text + ImGui::Text("VRAM Usage:"); + + // Use a centered text format for the numeric values + std::string vramText = std::format("{:.2f}GB/{:.2f}GB ({:.1f}%)", currentGpuUsage, totalGpuMemory, 100 * percent); + float textWidth = ImGui::CalcTextSize(vramText.c_str()).x; + float windowWidth = ImGui::GetWindowWidth(); + + // Center the text if it fits within the window + if (textWidth < windowWidth) { + ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); + ImGui::Text("%s", vramText.c_str()); + } else { + ImGui::Text("%s", vramText.c_str()); + } + + // Only move the progress bar, not the text + ImGui::ProgressBar(percent, ImVec2(ImGui::GetWindowWidth() * 0.9f, 0.0f), ""); + } else { + // Display a fallback message if we couldn't get the VRAM info + ImGui::Text("VRAM Usage: Not available"); + } + } + + ImGui::PopStyleVar(); // ItemSpacing + ImGui::SetWindowFontScale(1.0f); // Reset font scale + + ImGui::End(); + ImGui::PopStyleVar(); // WindowBorderSize + ImGui::PopStyleColor(); // WindowBg +} + +void Menu::DrawPerformanceOverlaySettings() +{ + auto& themeSettings = settings.Theme; + + ImGui::Checkbox("Enable Performance Overlay", &settings.PerfOverlay.Enabled); + + if (settings.PerfOverlay.Enabled) { + ImGui::Indent(); + + // Display options + if (ImGui::CollapsingHeader("Display Options", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // FPS options + ImGui::Checkbox("Show FPS Counter", &settings.PerfOverlay.ShowFPS); + + bool isFrameGenerationActive = globals::upscaling && globals::upscaling->IsFrameGenerationActive(); + if (settings.PerfOverlay.ShowFPS) { + ImGui::Indent(); + + if (isFrameGenerationActive) { + // Pre-Frame Generation FPS + if (ImGui::TreeNodeEx("Pre-Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox("Show Pre-FG FPS", &settings.PerfOverlay.ShowPreFGFPS); + if (settings.PerfOverlay.ShowPreFGFPS) { + ImGui::Indent(); + ImGui::Checkbox("Show Pre-FG Frametime", &settings.PerfOverlay.ShowPreFGFrameTime); + ImGui::Checkbox("Show Pre-FG Frametime Graph", &settings.PerfOverlay.ShowPreFGFrameTimeGraph); + ImGui::Unindent(); + } + ImGui::TreePop(); + } + + // Post-Frame Generation FPS + if (ImGui::TreeNodeEx("Post-Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox("Show Post-FG FPS", &settings.PerfOverlay.ShowPostFGFPS); + if (settings.PerfOverlay.ShowPostFGFPS) { + ImGui::Indent(); + ImGui::Checkbox("Show Post-FG Frametime", &settings.PerfOverlay.ShowPostFGFrameTime); + ImGui::Checkbox("Show Post-FG Frametime Graph", &settings.PerfOverlay.ShowPostFGFrameTimeGraph); + ImGui::Unindent(); + } + ImGui::TreePop(); + } + } else { + // Regular FPS options when frame generation is not active + ImGui::Checkbox("Show Frametime", &settings.PerfOverlay.ShowPreFGFrameTime); + ImGui::Checkbox("Show Frametime Graph", &settings.PerfOverlay.ShowPreFGFrameTimeGraph); + } + + ImGui::Unindent(); + } + + ImGui::Checkbox("Show Draw Calls", &settings.PerfOverlay.ShowDrawCalls); + ImGui::Checkbox("Show VRAM Usage", &settings.PerfOverlay.ShowVRAM); + + ImGui::Unindent(); + } + // Hotkey settings + if (ImGui::CollapsingHeader("Hotkeys", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Add hotkey configuration for toggling overlay + if (settingOverlayToggleKey) { + ImGui::Text("Press any key to set as Performance Overlay toggle key..."); + } else { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Toggle Key:"); + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", KeyIdToString(settings.PerfOverlay.OverlayToggleKey)); + ImGui::AlignTextToFramePadding(); + ImGui::SameLine(); + if (ImGui::Button("Change##overlayToggle")) { + settingOverlayToggleKey = true; + } + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Set a key to show/hide the performance overlay"); + } + ImGui::Unindent(); + } + + // Appearance settings + if (ImGui::CollapsingHeader("Appearance", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Text size options + const char* sizes[] = { "Small", "Medium", "Large" }; + int currentSize = static_cast(settings.PerfOverlay.Size); + if (ImGui::Combo("Text Size", ¤tSize, sizes, IM_ARRAYSIZE(sizes))) { + settings.PerfOverlay.Size = static_cast(currentSize); + } + + // Background opacity slider + ImGui::SliderFloat("Background Opacity", &settings.PerfOverlay.BackgroundOpacity, 0.0f, 1.0f, "%.2f"); + + // Border toggle + ImGui::Checkbox("Show Border", &settings.PerfOverlay.ShowBorder); + + // FPS update interval slider - Make this slider affect all FPS and frametime displays + ImGui::SliderFloat("Update Interval", &settings.PerfOverlay.UpdateInterval, 0.001f, 2.0f, "%.2f seconds"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How frequently all performance metrics should update (FPS and frametime)"); + } + + // Position options - moved inside appearance section + ImGui::Separator(); + ImGui::Text("Position:"); + + // Reset position button + if (ImGui::Button("Reset Position")) { + settings.PerfOverlay.PositionSet = false; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Reset the position of the performance overlay to default"); + } + + ImGui::Unindent(); + } + + ImGui::Unindent(); + } +} + const ImGuiKey Menu::VirtualKeyToImGuiKey(WPARAM vkKey) { switch (vkKey) { @@ -1445,6 +1933,9 @@ void Menu::ProcessInputEventQueue() } else if (settingsEffectsToggle) { settings.EffectToggleKey = key; settingsEffectsToggle = false; + } else if (settingOverlayToggleKey) { + settings.PerfOverlay.OverlayToggleKey = key; + settingOverlayToggleKey = false; } else if (key == settings.ToggleKey) { IsEnabled = !IsEnabled; } else if (key == settings.SkipCompilationKey) { @@ -1459,6 +1950,8 @@ void Menu::ProcessInputEventQueue() } else if (key == nextShaderKey && globals::state->IsDeveloperMode()) { auto shaderCache = globals::shaderCache; shaderCache->IterateShaderBlock(false); + } else if (key == settings.PerfOverlay.OverlayToggleKey) { + settings.PerfOverlay.Enabled = !settings.PerfOverlay.Enabled; } if (key == VK_ESCAPE && IsEnabled) { IsEnabled = false; diff --git a/src/Menu.h b/src/Menu.h index 8941f67741..68ec31f1bd 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -39,6 +39,7 @@ class Menu void Init(); void DrawSettings(); void DrawOverlay(); + void DrawPerfOverlay(); void ProcessInputEvents(RE::InputEvent* const* a_events); bool ShouldSwallowInput(); @@ -143,6 +144,34 @@ class Menu uint32_t SkipCompilationKey = VK_ESCAPE; uint32_t EffectToggleKey = VK_MULTIPLY; // toggle all effects ThemeSettings Theme; + + struct PerfOverlaySettings + { + bool Enabled = false; + bool ShowDrawCalls = true; + bool ShowVRAM = true; + bool ShowFPS = true; + bool ShowPreFGFrameTime = true; + bool ShowPreFGFrameTimeGraph = true; + bool ShowPreFGFPS = true; + bool ShowPostFGFPS = true; + bool ShowPostFGFrameTime = true; + bool ShowPostFGFrameTimeGraph = true; + float UpdateInterval = 0.5f; + enum class TextSize + { + Small, + Medium, + Large + }; + TextSize Size = TextSize::Medium; + + float BackgroundOpacity = 0.5f; + bool ShowBorder = true; + ImVec2 Position = ImVec2(10.f, 10.f); + bool PositionSet = false; + uint32_t OverlayToggleKey = VK_F10; + } PerfOverlay; }; private: @@ -154,6 +183,7 @@ class Menu bool settingToggleKey = false; bool settingSkipCompilationKey = false; bool settingsEffectsToggle = false; + bool settingOverlayToggleKey = false; uint32_t testInterval = 0; // Seconds to wait before toggling user/test settings bool inTestMode = false; // Whether we're in test mode bool usingTestConfig = false; // Whether we're using the test config @@ -170,6 +200,7 @@ class Menu void DrawDisplaySettings(); void DrawDisableAtBootSettings(); void DrawFooter(); + void DrawPerformanceOverlaySettings(); class CharEvent : public RE::InputEvent { diff --git a/src/State.cpp b/src/State.cpp index 2827c32316..b5a053e104 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -77,10 +77,17 @@ void State::Draw() currentExtraFeatureDescriptor = 0; if (frameChecker.IsNewFrame()) { + for (int i = 0; i < RE::BSShader::Type::Total + 1; ++i) + smoothDrawCalls[i] = smoothDrawCalls[i] * 0.95 + drawCalls[i] * 0.05; + for (auto& c : drawCalls) + c = 0; ID3D11Buffer* buffers[3] = { permutationCB->CB(), sharedDataCB->CB(), featureDataCB->CB() }; context->PSSetConstantBuffers(4, 3, buffers); context->CSSetConstantBuffers(5, 2, buffers + 1); } + drawCalls[RE::BSShader::Type::Total]++; + if (currentShader) + drawCalls[currentShader->shaderType.get()]++; if (currentShader && updateShader) { auto type = currentShader->shaderType.get(); @@ -496,6 +503,10 @@ void State::ModifyRenderTarget(RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::B void State::SetupResources() { + for (auto& c : drawCalls) + c = 0; + for (auto& c : smoothDrawCalls) + c = 0; auto renderer = globals::game::renderer; permutationCB = new ConstantBuffer(ConstantBufferDesc()); diff --git a/src/State.h b/src/State.h index 827263cf8c..8bff20f46e 100644 --- a/src/State.h +++ b/src/State.h @@ -44,6 +44,8 @@ class State bool upscalerLoaded = false; float timer = 0; + double smoothDrawCalls[RE::BSShader::Type::Total + 1]; + int drawCalls[RE::BSShader::Type::Total + 1]; enum ConfigMode { diff --git a/src/Upscaling.cpp b/src/Upscaling.cpp index 4a927ed3bd..10c66b95c3 100644 --- a/src/Upscaling.cpp +++ b/src/Upscaling.cpp @@ -759,4 +759,23 @@ double Upscaling::GetRefreshRate(HWND a_window) } logger::error("Failed to retrieve refresh rate from swap chain"); return 60; +} + +bool Upscaling::IsFrameGenerationActive() const +{ + return d3d12Interop && settings.frameGenerationMode; +} + +float Upscaling::GetFrameGenerationFrameTime() const +{ + if (!IsFrameGenerationActive()) + return 0.0f; + + // Get the current frame time from D3D12 swapchain + if (globals::dx12SwapChain && globals::dx12SwapChain->swapChain) { + // Get frame time from the D3D12 SwapChain + return globals::dx12SwapChain->GetFrameTime(); + } + + return 0.0f; } \ No newline at end of file diff --git a/src/Upscaling.h b/src/Upscaling.h index aa9e17d003..c132c9836c 100644 --- a/src/Upscaling.h +++ b/src/Upscaling.h @@ -47,6 +47,10 @@ class Upscaling bool d3d12Interop = false; double refreshRate = 0.0f; + // FG FPS Measurement for Overlay + bool IsFrameGenerationActive() const; + float GetFrameGenerationFrameTime() const; + void DrawSettings(); void SaveSettings(json& o_json); void LoadSettings(json& o_json);