diff --git a/src/FidelityFX.cpp b/src/FidelityFX.cpp index 3656e8dfdd..958c2463cc 100644 --- a/src/FidelityFX.cpp +++ b/src/FidelityFX.cpp @@ -1,4 +1,5 @@ #include "FidelityFX.h" +#include "Utils/FileSystem.h" #include "State.h" #include "Upscaling.h" @@ -8,6 +9,9 @@ ffxFunctions ffxModule; +// Define the static member +std::vector> FidelityFX::dllVersions = {}; + FfxResource ffxGetResource(ID3D11Resource* dx11Resource, [[maybe_unused]] wchar_t const* ffxResName, FfxResourceStates state /*=FFX_RESOURCE_STATE_COMPUTE_READ*/) @@ -28,7 +32,12 @@ FfxResource ffxGetResource(ID3D11Resource* dx11Resource, void FidelityFX::LoadFFX() { - module = LoadLibrary(L"Data\\SKSE\\Plugins\\FidelityFX\\amd_fidelityfx_dx12.dll"); + std::wstring dllPath = std::wstring(FidelityFX::PluginDir) + L"\\amd_fidelityfx_dx12.dll"; + module = LoadLibrary(dllPath.c_str()); + + // Cache all DLL versions in the FidelityFX directory + std::filesystem::path pluginDir = std::filesystem::path(FidelityFX::PluginDir); + FidelityFX::dllVersions = Util::EnumerateDllVersions(pluginDir); if (module) ffxLoadFunctions(&ffxModule, module); @@ -48,7 +57,10 @@ void FidelityFX::SetupFrameGeneration() createBackend.device = swapChain->d3d12Device.get(); if (ffx::CreateContext(frameGenContext, nullptr, createFg, createBackend) != ffx::ReturnCode::Ok) { + featureFSR3FG = false; logger::critical("[FidelityFX] Failed to create frame generation context!"); + } else { + featureFSR3FG = true; } } @@ -184,6 +196,9 @@ void FidelityFX::Present(bool a_useFrameGeneration) } frameID++; + + // Set isFrameGenActive based on whether FSR3 frame generation is enabled + isFrameGenActive = a_useFrameGeneration; } void FidelityFX::CreateFSRResources() diff --git a/src/FidelityFX.h b/src/FidelityFX.h index df70251c08..e7a63a373d 100644 --- a/src/FidelityFX.h +++ b/src/FidelityFX.h @@ -16,6 +16,8 @@ class FidelityFX { public: + static constexpr const wchar_t* PluginDir = L"Data\\SKSE\\Plugins\\FidelityFX"; + static FidelityFX* GetSingleton() { static FidelityFX singleton; @@ -29,6 +31,14 @@ class FidelityFX FfxFsr3Context fsrContext; + bool featureFSR3FG = false; // whether enabled + + // Track if FidelityFX is currently being used for frame generation + bool isFrameGenActive = false; + + // Cached DLL version info for FidelityFX plugin directory + static std::vector> dllVersions; + void LoadFFX(); void SetupFrameGeneration(); diff --git a/src/Streamline.cpp b/src/Streamline.cpp index 2b3788113b..590d541bff 100644 --- a/src/Streamline.cpp +++ b/src/Streamline.cpp @@ -13,24 +13,67 @@ void LoggingCallback(sl::LogType type, const char* msg) { + // Remove trailing newlines from the raw message + std::string rawMsg(msg); + while (!rawMsg.empty() && (rawMsg.back() == '\n' || rawMsg.back() == '\r')) + rawMsg.pop_back(); + + // Remove leading bracketed metadata + const char* p = msg; + while (*p == '[') { + const char* close = strchr(p, ']'); + if (!close) + break; + p = close + 1; + // Skip whitespace after each bracketed section + while (*p == ' ' || *p == '\t') ++p; + } + // Now p points to the first non-bracketed section (file/line info or message) + std::string cleanMsg(p); + // Trim leading/trailing whitespace and newlines + size_t start = cleanMsg.find_first_not_of(" \t\r\n"); + size_t end = cleanMsg.find_last_not_of(" \t\r\n"); + if (start != std::string::npos && end != std::string::npos) + cleanMsg = cleanMsg.substr(start, end - start + 1); + else + cleanMsg.clear(); + + // If the cleaned message is empty or only bracketed tokens, log the raw message + bool onlyBrackets = true; + for (char c : cleanMsg) { + if (c != '[' && c != ']' && c != ' ' && c != '\t') { + onlyBrackets = false; + break; + } + } + if (cleanMsg.empty() || onlyBrackets) { + logger::info("[StreamlineSDK:RAW] {}", rawMsg); + return; + } + + // Use a clear prefix + const char* prefix = "[StreamlineSDK]"; switch (type) { case sl::LogType::eInfo: - logger::info("{}", msg); + logger::info("{} {}", prefix, cleanMsg); break; case sl::LogType::eWarn: - logger::warn("{}", msg); + logger::warn("{} {}", prefix, cleanMsg); break; case sl::LogType::eError: - logger::error("{}", msg); + logger::error("{} {}", prefix, cleanMsg); break; } } +std::vector> Streamline::dllVersions = {}; + void Streamline::LoadInterposer() { triedInitialization = true; - interposer = LoadLibraryW(L"Data/SKSE/Plugins/Streamline/sl.interposer.dll"); + std::wstring interposerPath = std::wstring(Streamline::PluginDir) + L"\\sl.interposer.dll"; + interposer = LoadLibraryW(interposerPath.c_str()); if (interposer == nullptr) { DWORD errorCode = GetLastError(); logger::info("[Streamline] Failed to load interposer: Error Code {0:x}", errorCode); @@ -39,6 +82,23 @@ void Streamline::LoadInterposer() logger::info("[Streamline] Interposer loaded at address: {0:p}", static_cast(interposer)); } + // Dynamically log all DLL versions in the Streamline plugin directory + std::filesystem::path pluginDir = std::filesystem::path(Streamline::PluginDir); + Streamline::dllVersions.clear(); + for (const auto& entry : std::filesystem::directory_iterator(pluginDir)) { + if (entry.is_regular_file() && entry.path().extension() == L".dll") { + const auto& path = entry.path(); + auto version = Util::GetDllVersion(path.c_str()); + auto name = path.filename().string(); + std::string versionStr = version ? Util::GetFormattedVersion(*version) : "Unknown"; + Streamline::dllVersions.emplace_back(name, versionStr); + if (version) + logger::info("[Streamline] {} version: {}", name, versionStr); + else + logger::info("[Streamline] {} version: Unknown", name); + } + } + logger::info("[Streamline] Initializing Streamline"); sl::Preferences pref; @@ -49,7 +109,19 @@ void Streamline::LoadInterposer() pref.featuresToLoad = REL::Module::IsVR() ? featuresToLoadVR : featuresToLoad; pref.numFeaturesToLoad = _countof(featuresToLoad); - pref.logLevel = sl::LogLevel::eOff; + // Set log level from settings + switch (globals::upscaling->settings.streamlineLogLevel) { + case 2: + pref.logLevel = sl::LogLevel::eVerbose; + break; + case 1: + pref.logLevel = sl::LogLevel::eDefault; + break; + case 0: + default: + pref.logLevel = sl::LogLevel::eOff; + break; + } pref.logMessageCallback = LoggingCallback; pref.showConsole = false; @@ -321,6 +393,9 @@ void Streamline::Present() static auto currentFrameGenerationMode = sl::DLSSGMode::eOff; auto frameGenerationMode = upscaling->settings.frameGenerationMode ? sl::DLSSGMode::eOn : sl::DLSSGMode::eOff; + // Set isFrameGenActive based on whether DLSS-G frame generation is enabled + isFrameGenActive = (frameGenerationMode == sl::DLSSGMode::eOn); + if (currentFrameGenerationMode != frameGenerationMode) { currentFrameGenerationMode = frameGenerationMode; diff --git a/src/Streamline.h b/src/Streamline.h index c19a9e34ea..a81f1680eb 100644 --- a/src/Streamline.h +++ b/src/Streamline.h @@ -92,6 +92,8 @@ void DestroyDLSSResources(); class Streamline { public: + static constexpr const wchar_t* PluginDir = L"Data\\SKSE\\Plugins\\Streamline"; + static Streamline* GetSingleton() { static Streamline singleton; @@ -108,6 +110,9 @@ class Streamline bool featureDLSSG = false; bool featureReflex = false; + // Track if Streamline is currently being used for frame generation + bool isFrameGenActive = false; + sl::ViewportHandle viewport{ 0 }; HMODULE interposer = NULL; @@ -152,6 +157,9 @@ class Streamline decltype(&CreateDXGIFactory1) slCreateDXGIFactory1{}; decltype(&D3D11CreateDeviceAndSwapChain) slD3D11CreateDeviceAndSwapChain{}; + // Cached DLL version info for Streamline plugin directory + static std::vector> dllVersions; + void LoadInterposer(); void CheckFeatures(IDXGIAdapter* a_adapter); diff --git a/src/Upscaling.cpp b/src/Upscaling.cpp index 2b85500b22..99209703a7 100644 --- a/src/Upscaling.cpp +++ b/src/Upscaling.cpp @@ -3,6 +3,7 @@ #include "DX12SwapChain.h" #include "Hooks.h" #include "State.h" +#include #include NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -14,7 +15,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( dlssPreset, frameLimitMode, frameGenerationMode, - frameGenerationForceEnable); + frameGenerationForceEnable, + streamlineLogLevel); void Upscaling::DrawSettings() { @@ -88,54 +90,108 @@ void Upscaling::DrawSettings() } if (!globals::game::isVR) { - if (ImGui::TreeNodeEx("Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Frame Generation interpolates real frames with generated ones for a smoother experience"); - ImGui::Text("Uses AMD FSR 3.1 Frame Generation and NVIDIA DLSS Frame Generation technology"); - ImGui::Text("Requires a D3D11 to D3D12 proxy which can create compatibility issues"); - ImGui::Text("Toggling this setting requires a restart to work correctly"); - - bool onlyRequiresRestart = true; + bool frameGenAvailable = d3d12Interop && ((globals::streamline && globals::streamline->featureDLSSG) || + (globals::fidelityFX && globals::fidelityFX->featureFSR3FG)); + if (frameGenAvailable) { + if (ImGui::TreeNodeEx("Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Frame Generation interpolates real frames with generated ones for a smoother experience"); + ImGui::Text("Uses AMD FSR 3.1 Frame Generation and NVIDIA DLSS Frame Generation technology"); + if (globals::streamline && globals::streamline->featureDLSSG) + ImGui::Text("NVIDIA DLSS-G Frame Generation is available."); + if (globals::fidelityFX && globals::fidelityFX->featureFSR3FG) + ImGui::Text("AMD FSR 3.1 Frame Generation is available."); + ImGui::Text("Requires a D3D11 to D3D12 proxy which can create compatibility issues"); + ImGui::Text("Toggling this setting requires a restart to work correctly"); + + bool onlyRequiresRestart = true; + + if (!isWindowed) { + ImGui::Text("Warning: Requires windowed mode"); + onlyRequiresRestart = false; + } - if (!isWindowed) { - ImGui::Text("Warning: Requires windowed mode"); - onlyRequiresRestart = false; - } + if (lowRefreshRate && !settings.frameGenerationForceEnable) { + ImGui::Text("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); + onlyRequiresRestart = false; + } - if (lowRefreshRate && !settings.frameGenerationForceEnable) { - ImGui::Text("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); - onlyRequiresRestart = false; - } + if (streamlineMissing) { + ImGui::Text("Warning: Streamline is not loaded"); + onlyRequiresRestart = false; + } - if (streamlineMissing) { - ImGui::Text("Warning: amd_fidelityfx_dx12.dll is not loaded"); - onlyRequiresRestart = false; - } + if (fidelityFXMissing) { + ImGui::Text("Warning: amd_fidelityfx_dx12.dll is not loaded"); + onlyRequiresRestart = false; + } - if (fidelityFXMissing) { - ImGui::Text("Warning: Streamline is not loaded"); - onlyRequiresRestart = false; - } + if (onlyRequiresRestart && settings.frameGenerationMode && !d3d12Interop) + ImGui::Text("Warning: Requires restart"); - if (onlyRequiresRestart && settings.frameGenerationMode && !d3d12Interop) - ImGui::Text("Warning: Requires restart"); + std::string backendLabel = + (globals::streamline && globals::streamline->isFrameGenActive) ? "DLSSG" : + (globals::fidelityFX && globals::fidelityFX->isFrameGenActive) ? "FSR3" : + "None"; + std::string enabledLabel = "Enabled (" + backendLabel + ")"; + const char* toggleModes[] = { "Disabled", "Enabled" }; + const char* toggleModesFG[] = { "Disabled", enabledLabel.c_str() }; - const char* toggleModes[] = { "Disabled", "Enabled" }; + ImGui::SliderInt("Frame Generation", (int*)&settings.frameGenerationMode, 0, 1, toggleModesFG[settings.frameGenerationMode]); - ImGui::SliderInt("Frame Generation", (int*)&settings.frameGenerationMode, 0, 1, std::format("{}", toggleModes[settings.frameGenerationMode]).c_str()); + if (!d3d12Interop) + ImGui::BeginDisabled(); - if (!d3d12Interop) - ImGui::BeginDisabled(); + ImGui::SliderInt("Frame Limit (Variable Refresh Rate)", (int*)&settings.frameLimitMode, 0, 1, std::format("{}", toggleModes[settings.frameLimitMode]).c_str()); - ImGui::SliderInt("Frame Limit (Variable Refresh Rate)", (int*)&settings.frameLimitMode, 0, 1, std::format("{}", toggleModes[settings.frameLimitMode]).c_str()); + if (!d3d12Interop) + ImGui::EndDisabled(); - if (!d3d12Interop) - ImGui::EndDisabled(); + ImGui::Text("Allows frame generation to function on low refresh rate monitors"); + ImGui::SliderInt("Force Enable Frame Generation", (int*)&settings.frameGenerationForceEnable, 0, 1, std::format("{}", toggleModes[settings.frameGenerationForceEnable]).c_str()); - ImGui::Text("Allows frame generation to function on low refresh rate monitors"); - ImGui::SliderInt("Force Enable Frame Generation", (int*)&settings.frameGenerationForceEnable, 0, 1, std::format("{}", toggleModes[settings.frameGenerationForceEnable]).c_str()); + ImGui::TreePop(); + } + } else { + if (ImGui::TreeNodeEx("Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Frame Generation is not available on your system.\nThis requires either NVIDIA DLSS-G or AMD FSR 3.1 Frame Generation support and D3D12 interop."); + ImGui::TreePop(); + } + } + } - ImGui::TreePop(); + if (ImGui::TreeNodeEx("Backend Diagnostics")) { + // Streamline log level selection + const char* logLevels[] = { "Off", "Default", "Verbose" }; + int logLevelIdx = static_cast(settings.streamlineLogLevel); + if (ImGui::Combo("Streamline Logging", &logLevelIdx, logLevels, IM_ARRAYSIZE(logLevels))) { + settings.streamlineLogLevel = static_cast(logLevelIdx); + } + ImGui::TextUnformatted("Changing this requires a restart to take effect."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G."); + } + ImGui::Separator(); + // FidelityFX section + if (ImGui::Selectable("AMD FidelityFX DLLs (click to open folder)")) { + ShellExecuteW(nullptr, L"open", FidelityFX::PluginDir, nullptr, nullptr, SW_SHOWNORMAL); + } + std::vector headers = { "DLL Name", "Version" }; + std::vector> ffRows; + for (const auto& [name, version] : FidelityFX::dllVersions) + ffRows.push_back({ name, version }); + std::vector ffSorters = { nullptr, Util::VersionSortComparator }; + Util::ShowSortedStringTable("ffx_dll_versions", headers, ffRows, 0, true, ffSorters); + + // Streamline section + if (ImGui::Selectable("NVIDIA Streamline DLLs (click to open folder)")) { + ShellExecuteW(nullptr, L"open", Streamline::PluginDir, nullptr, nullptr, SW_SHOWNORMAL); } + std::vector> slRows; + for (const auto& [name, version] : Streamline::dllVersions) + slRows.push_back({ name, version }); + std::vector slSorters = { nullptr, Util::VersionSortComparator }; + Util::ShowSortedStringTable("sl_dll_versions", headers, slRows, 0, true, slSorters); + ImGui::TreePop(); } } diff --git a/src/Upscaling.h b/src/Upscaling.h index 522ea99ac3..4becbdba2e 100644 --- a/src/Upscaling.h +++ b/src/Upscaling.h @@ -41,6 +41,7 @@ class Upscaling uint frameLimitMode = 1; uint frameGenerationMode = 1; uint frameGenerationForceEnable = 0; + uint streamlineLogLevel = 0; // 0=Off, 1=Default, 2=Verbose }; Settings settings; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index 813b602fed..8176a6f52e 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -1,5 +1,15 @@ #pragma once +#include "FileSystem.h" +#include "Format.h" +#include "Winapi.h" +#include +#include +#include +#include +#include +#include + namespace Util { /** @@ -64,4 +74,28 @@ namespace Util */ DeletionResult SafeDelete(const std::string& path, const std::string& description); } + + /** + * Enumerates all DLLs in a directory and returns a vector of (name, version string) pairs. + */ + inline std::vector> EnumerateDllVersions(const std::filesystem::path& dir) + { + std::vector> result; + try { + for (const auto& entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file() && entry.path().extension() == L".dll") { + const auto& path = entry.path(); + auto version = Util::GetDllVersion(path.c_str()); + auto name = path.filename().string(); + std::string versionStr = version ? Util::GetFormattedVersion(*version) : "Unknown"; + result.emplace_back(name, versionStr); + } + } + } catch (const std::filesystem::filesystem_error& e) { + // Log error but return empty vector to avoid crashing + logger::warn("Failed to enumerate DLL versions in {}: {}", dir.string(), e.what()); + } + return result; + } + } diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index e0b925f557..d1fd318b56 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -9,8 +9,12 @@ #include "../Menu.h" #define STB_IMAGE_IMPLEMENTATION +#include #include +#include #include +#include +#include namespace Util { @@ -548,4 +552,97 @@ namespace Util } } + void SortTableRowsByColumn(std::vector>& rows, size_t column, bool ascending) + { + std::sort(rows.begin(), rows.end(), [column, ascending](const auto& a, const auto& b) { + if (column >= a.size() || column >= b.size()) + return false; + return ascending ? (a[column] < b[column]) : (a[column] > b[column]); + }); + } + + bool VersionStringLess(const std::string& a, const std::string& b, bool ascending) + { + auto split = [](const std::string& s) { + std::vector parts; + size_t start = 0, end = 0; + while ((end = s.find('.', start)) != std::string::npos) { + try { + parts.push_back(std::stoi(s.substr(start, end - start))); + } catch (...) { + parts.push_back(0); + } + start = end + 1; + } + if (start < s.size()) { + try { + parts.push_back(std::stoi(s.substr(start))); + } catch (...) { + parts.push_back(0); + } + } + return parts; + }; + auto va = split(a), vb = split(b); + for (size_t i = 0; i < std::max(va.size(), vb.size()); ++i) { + int ai = i < va.size() ? va[i] : 0; + int bi = i < vb.size() ? vb[i] : 0; + if (ai != bi) + return ascending ? (ai < bi) : (ai > bi); + } + return false; + } + + const TableSortFunc VersionSortComparator = [](const std::string& a, const std::string& b, bool asc) { + return VersionStringLess(a, b, asc); + }; + + void ShowSortedStringTable( + const char* table_id, + const std::vector& headers, + std::vector> rows, + size_t sortColumn, + bool ascending, + const std::vector& customSorts) + { + ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable; + if (ImGui::BeginTable(table_id, static_cast(headers.size()), flags)) { + for (const auto& header : headers) + ImGui::TableSetupColumn(header.c_str()); + ImGui::TableHeadersRow(); + + // Interactive sorting + int sortCol = static_cast(sortColumn); + bool sortAsc = ascending; + if (const ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { + if (sortSpecs->SpecsCount > 0) { + sortCol = sortSpecs->Specs->ColumnIndex; + sortAsc = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + } + } + if (sortCol >= 0 && static_cast(sortCol) < headers.size()) { + if (sortCol < static_cast(customSorts.size()) && customSorts[sortCol]) { + auto cmp = customSorts[sortCol]; + std::sort(rows.begin(), rows.end(), [sortCol, sortAsc, &cmp](const auto& a, const auto& b) { + return cmp(a[sortCol], b[sortCol], sortAsc); + }); + } else { + std::sort(rows.begin(), rows.end(), [sortCol, sortAsc](const auto& a, const auto& b) { + return sortAsc ? (a[sortCol] < b[sortCol]) : (a[sortCol] > b[sortCol]); + }); + } + } + // else: no sorting if sortCol is invalid + + for (const auto& row : rows) { + ImGui::TableNextRow(); + for (size_t col = 0; col < headers.size(); ++col) { + ImGui::TableSetColumnIndex(static_cast(col)); + if (col < row.size()) + ImGui::TextUnformatted(row[col].c_str()); + } + } + ImGui::EndTable(); + } + } } // namespace Util \ No newline at end of file diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 4fc41990a9..d175d4a79e 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1,5 +1,9 @@ #pragma once +#include +#include #include +#include +#include // Forward declarations struct ID3D11Device; @@ -213,4 +217,55 @@ namespace Util * @param colors Optional per-line colors (if empty, default color is used for all lines). */ void DrawMultiLineTooltip(const std::vector& lines, const std::vector& colors = {}); + + /** + * @brief Comparator function type for table sorting. + * + * Should return true if the first value should come before the second, given the sort direction. + * @param a First string value (cell content). + * @param b Second string value (cell content). + * @param ascending True if sorting ascending, false for descending. + * @return True if a should come before b, false otherwise. + */ + using TableSortFunc = std::function; + + /** + * @brief Sorts table rows by the specified column using a default string comparison. + * @param rows The table data (vector of rows). + * @param column The column index to sort by. + * @param ascending True for ascending, false for descending. + */ + void SortTableRowsByColumn(std::vector>& rows, size_t column, bool ascending = true); + + /** + * @brief Renders a sortable ImGui table with arbitrary columns and per-column custom sorting. + * + * @param table_id Unique ImGui table ID. + * @param headers Column headers. + * @param rows Table data, each row is a vector of strings. + * @param sortColumn Default sort column index. + * @param ascending Default sort direction. + * @param customSorts Vector of custom comparator functions, one per column (nullptr for default string sort). + */ + void ShowSortedStringTable( + const char* table_id, + const std::vector& headers, + std::vector> rows, + size_t sortColumn = 0, + bool ascending = true, + const std::vector& customSorts = {}); + + /** + * @brief Compares two version strings (e.g., "1.2.3") numerically. + * @param a First version string. + * @param b Second version string. + * @param ascending True for ascending, false for descending. + * @return True if a < b (or a > b if ascending is false). + */ + bool VersionStringLess(const std::string& a, const std::string& b, bool ascending = true); + + /** + * @brief TableSortFunc for version strings, using VersionStringLess. + */ + extern const TableSortFunc VersionSortComparator; } // namespace Util