diff --git a/.gitmodules b/.gitmodules index d4fb422d95..0e533617aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "extern/FidelityFX-SDK"] path = extern/FidelityFX-SDK url = https://github.com/MapleHinata/FidelityFX-SDK +[submodule "extern/nvapi"] + path = extern/nvapi + url = https://github.com/NVIDIA/nvapi.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 771a3df09a..c004ad2a5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,9 @@ find_package(unordered_dense CONFIG REQUIRED) find_package(efsw CONFIG REQUIRED) find_package(Tracy CONFIG REQUIRED) find_package(directx-headers CONFIG REQUIRED) + add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) +add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/NVAPI) find_path(DETOURS_INCLUDE_DIRS "detours/detours.h") find_library(DETOURS_LIBRARY detours REQUIRED) @@ -139,6 +141,7 @@ target_link_libraries( efsw::efsw Tracy::TracyClient Streamline + NVAPI d3d12.lib Microsoft::DirectX-Headers ${DETOURS_LIBRARY} diff --git a/cmake/NVAPI/CMakeLists.txt b/cmake/NVAPI/CMakeLists.txt new file mode 100644 index 0000000000..794f5e888c --- /dev/null +++ b/cmake/NVAPI/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.21) + +add_library(NVAPI INTERFACE) + +target_include_directories( + NVAPI INTERFACE ${CMAKE_SOURCE_DIR}/extern/nvapi +) + +target_link_libraries( + NVAPI INTERFACE "${CMAKE_SOURCE_DIR}/extern/nvapi/amd64/nvapi64.lib" +) + +if(MSVC) + # NVAPI headers use Latin-1 encoding (© in copyright notices); suppress C4828 + # which fires when the compiler parses them as UTF-8. + target_compile_options(NVAPI INTERFACE /wd4828) +endif() diff --git a/extern/nvapi b/extern/nvapi new file mode 160000 index 0000000000..3a83ef27ed --- /dev/null +++ b/extern/nvapi @@ -0,0 +1 @@ +Subproject commit 3a83ef27ed8bd1a273cb667d3425b68c3c5f2933 diff --git a/features/VRS/Shaders/Features/VRS.ini b/features/VRS/Shaders/Features/VRS.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/VRS/Shaders/Features/VRS.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 4749feeaae..7117d8635a 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -14,6 +14,7 @@ #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRS.h" #include "Features/WeatherEditor.h" #include "Hooks.h" @@ -665,7 +666,9 @@ void Deferred::Hooks::Main_RenderWorld_BlendedDecals::thunk(RE::BSShaderAccumula auto& terrainBlending = globals::features::terrainBlending; // Defer terrain rendering until after everything else if (terrainBlending.loaded && terrainBlending.settings.Enabled) { + globals::features::vrs.SuspendVRS(); terrainBlending.RenderTerrainBlendingPasses(); + globals::features::vrs.ResumeVRS(); } } diff --git a/src/Feature.cpp b/src/Feature.cpp index ab2a8db684..43a31dbc2b 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -31,6 +31,7 @@ #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRS.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -236,6 +237,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::ibl, &globals::features::extendedTranslucency, &globals::features::upscaling, + &globals::features::vrs, &globals::features::renderDoc, &globals::features::weatherEditor, &globals::features::linearLighting, diff --git a/src/Features/VRS.cpp b/src/Features/VRS.cpp new file mode 100644 index 0000000000..80e0b0331f --- /dev/null +++ b/src/Features/VRS.cpp @@ -0,0 +1,295 @@ +#include "VRS.h" + +#include "Globals.h" +#include "Hooks.h" +#include "State.h" +#include "Upscaling.h" +#include "Utils/UI.h" + +#include + +#include "RE/S/ShaderAccumulator.h" + +// Terrain Blending compatibility: VRS is suspended around TB's deferred terrain passes. +// Note: VRS reduces pixel shader overhead at high resolutions but does not affect compute shader cost; further adaptation needed. + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + VRS::Settings, + vrEnableVRS, + vrVRSSrsPreset, + vrVRSLutPreset, + vrVRSRingGrowthRate, + vrEnableDirectionalRates, + vrEnableBoundaryDither, + vrEnableDiagnostics); + +void VRS::PostPostLoad() +{ + if (!globals::game::isVR) { + logger::info("[VRS] Not VR runtime, skipping hook installation"); + return; + } + + const bool isGOG = !GetModuleHandle(L"steam_api64.dll"); + + // Hook the same UpdateJitter call site as Upscaling. + // write_thunk_call chains: VRS (last writer) → Upscaling → original. + // VRS must be installed AFTER Upscaling (both in PostPostLoad via + // ForEachLoadedFeature order) so VRS::thunk wraps and preserves + // the Upscaling thunk in its func pointer. + stl::write_thunk_call(REL::RelocationID(75460, 77245).address() + REL::Relocate(0xE5, isGOG ? 0x133 : 0xE2, 0x104)); + + // Hook BSShaderAccumulator::FinishAccumulatingDispatch (vtable 0x2A) + // to disable VRS before the UI pass (renderMode 24), which runs at + // display resolution and must not be affected by foveated cull. + stl::write_vfunc<0x2A, Main_FinishAccumulatingDispatch>( + RE::VTABLE_BSShaderAccumulator[0]); + + hardwareAvailable_ = true; + logger::info("[VRS] Installed hooks (frame scoped)"); +} + +void VRS::Main_UpdateJitter::thunk(RE::BSGraphics::State* a_state) +{ + func(a_state); + globals::features::vrs.UpdateVRShadingRateState(); +} + +void VRS::Main_FinishAccumulatingDispatch::thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags) +{ + // renderMode 24 = UI pass, which runs at display resolution (DRS=1.0). + // VRS must be disabled before the UI pass to avoid foveated cull/low-rate + // artifacts on HUD elements. + if (shaderAccumulator && shaderAccumulator->GetRuntimeData().renderMode == 24) { + globals::features::vrs.DisableVRShadingRateState(); + } + func(shaderAccumulator, renderFlags); +} + +void VRS::DrawSettings() +{ + settings.vrEnableVRS = std::min(settings.vrEnableVRS, 1u); + const char* vrsToggle[] = { "Disabled", "Enabled" }; + ImGui::SliderInt("VR NVAPI VRS", (int*)&settings.vrEnableVRS, 0, 1, vrsToggle[settings.vrEnableVRS]); + ImGui::TextDisabled("Foveated rendering matched to human eye acuity. Most impactful on mid/low-end GPUs."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Reduces pixel shading rate in peripheral vision using concentric elliptical zones."); + ImGui::Text("Directional-adaptive rates (2x1/1x2) preserve detail along natural eye-tracking axes."); + ImGui::Text("Terrain Blending: terrain always renders at 1x1 when TB is active."); + } + + settings.vrVRSSrsPreset = std::min(settings.vrVRSSrsPreset, 2u); + const char* srsPresets[] = { "Default", "Faster", "Extreme" }; + ImGui::SliderInt("VRS Rate Preset", (int*)&settings.vrVRSSrsPreset, 0, 2, srsPresets[settings.vrVRSSrsPreset]); + static constexpr const char* kPresetHint[] = { + "6 rings: 1x1 > Half > 2x2 > Eighth > 4x4 > Cull", + "4 rings: 1x1 > 2x2 > 4x4 > Cull", + "3 rings: 1x1 > 4x4 > Cull", + }; + ImGui::TextDisabled("%s", kPresetHint[settings.vrVRSSrsPreset]); + + settings.vrVRSLutPreset = std::min(settings.vrVRSLutPreset, 2u); + const char* lutPresets[] = { "Default", "Full 1x1 (debug)", "Full 4x4 (debug)" }; + ImGui::SliderInt("LUT Override", (int*)&settings.vrVRSLutPreset, 0, 2, lutPresets[settings.vrVRSLutPreset]); + if (auto _ttLutOvr = Util::HoverTooltipWrapper()) { + ImGui::Text("Default: normal mapping. Debug overrides force a single uniform rate."); + } + + ImGui::Checkbox("Directional Rates", &settings.vrEnableDirectionalRates); + if (auto _ttDir = Util::HoverTooltipWrapper()) { + ImGui::Text("Adapts shading orientation to human peripheral vision: preserves horizontal"); + ImGui::Text("detail on left/right edges, vertical detail on top/bottom edges."); + } + ImGui::Checkbox("Boundary Dither", &settings.vrEnableBoundaryDither); + if (auto _ttDith = Util::HoverTooltipWrapper()) { + ImGui::Text("Checkerboard dithering at ring boundaries to soften transitions."); + } + + ImGui::SliderFloat("Ring Growth Rate", &settings.vrVRSRingGrowthRate, 0.05f, 1.0f, "%.2f"); + ImGui::TextDisabled("How fast quality drops from center to edge. Smaller = more gradual, larger = sharper drop."); + if (auto _ttRing = Util::HoverTooltipWrapper()) { + ImGui::Text("0.25 means each ring is 25%% wider than the previous one."); + ImGui::Text("Lower values create more rings with gentler transitions."); + } + + { + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "Tip: adjust VR DepthBuffer Culling value to match VRS coverage for best results."); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "Select an appropriate VRS foveal region below (~50%% centered recommended)."); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "%s", "\xe2\x86\x93"); + ImGui::TextDisabled("If using DLSSEnhancer or Screenshot, select the same Subrect preset for consistent framing."); + ID3D11ShaderResourceView* previewSrv = nullptr; + ID3D11Texture2D* previewTex = nullptr; + if (auto renderer = globals::game::renderer) { + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + previewSrv = reinterpret_cast(main.SRV); + previewTex = reinterpret_cast(main.texture); + } + subrectCtrl.DrawEditor(previewSrv, previewTex, 0.5f); + } + + ImGui::Checkbox("Enable Diagnostics", &settings.vrEnableDiagnostics); + if (auto _ttDiag = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable tile statistics, debug visualization, and diagnostics panel."); + ImGui::Text("Adds minor per-rebuild overhead (~80KB upload + O(tiles) stats)."); + } + + if (settings.vrEnableDiagnostics && ImGui::TreeNodeEx("VRS Diagnostics")) { + auto vrsState = nvVrs.GetDebugState(); + auto activeRateLabel = [](uint32_t level) -> const char* { + switch (level) { + case 0: + return "1x1"; + case 1: + return "2x1"; + case 2: + return "1x2"; + case 3: + return "2x2"; + case 4: + return "4x2"; + case 5: + return "2x4"; + case 6: + return "4x4"; + case 7: + return "Cull"; + default: + return "Unknown"; + } + }; + auto drawLegendRow = [](const char* label, const char* rate, ImVec4 color) { + ImGui::ColorButton(label, color, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, ImVec2(14.0f, 14.0f)); + ImGui::SameLine(); + ImGui::Text("%s -> %s", label, rate); + }; + + ImGui::Text("NVAPI: %s | Active: %s", vrsState.supported ? "OK" : (vrsState.initialized ? "Unsupported" : "Not Init"), vrsState.active ? "Yes" : "No"); + ImGui::Text("Surface: %u x %u Viewports: %u", vrsState.tileWidth, vrsState.tileHeight, vrsState.lastViewportCount); + ImGui::Text("Pattern Rebuild/Reuse: %llu / %llu", vrsState.patternRebuildCount, vrsState.patternReuseCount); + + static constexpr const char* kDisableReasons[] = { "None", "SettingsDisabled", "InvalidContext", "InitFailed", "SurfaceFailed", "BindSurfaceFailed", "BindRateTableFailed", "UIPass" }; + static constexpr const char* kRebuildReasons[] = { "None", "FirstCreate", "ResolutionChanged" }; + const auto drIdx = static_cast(vrsState.lastDisableReason); + const auto rrIdx = static_cast(vrsState.lastRebuildReason); + ImGui::Text("Disable: %s Rebuild: %s", drIdx < std::size(kDisableReasons) ? kDisableReasons[drIdx] : "?", rrIdx < std::size(kRebuildReasons) ? kRebuildReasons[rrIdx] : "?"); + + if (vrsState.failureCount > 0) { + ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Failures: %llu Last: %s (nvapi=%d)", vrsState.failureCount, vrsState.lastFailureSite, vrsState.lastNvapiStatus); + } + + ImGui::Text("Tiles: %llu / %llu / %llu / %llu / %llu / %llu / %llu / %llu", + vrsState.lastTileLevelCount[0], + vrsState.lastTileLevelCount[1], + vrsState.lastTileLevelCount[2], + vrsState.lastTileLevelCount[3], + vrsState.lastTileLevelCount[4], + vrsState.lastTileLevelCount[5], + vrsState.lastTileLevelCount[6], + vrsState.lastTileLevelCount[7]); + ImGui::Separator(); + ImGui::Text("SRS Legend"); + drawLegendRow("L0", activeRateLabel(0), ImVec4(0.20f, 0.85f, 0.35f, 1.0f)); + drawLegendRow("L1", activeRateLabel(1), ImVec4(0.20f, 0.75f, 0.85f, 1.0f)); + drawLegendRow("L2", activeRateLabel(2), ImVec4(0.75f, 0.85f, 0.20f, 1.0f)); + drawLegendRow("L3", activeRateLabel(3), ImVec4(0.95f, 0.80f, 0.20f, 1.0f)); + drawLegendRow("L4", activeRateLabel(4), ImVec4(0.98f, 0.55f, 0.15f, 1.0f)); + drawLegendRow("L5", activeRateLabel(5), ImVec4(0.95f, 0.45f, 0.30f, 1.0f)); + drawLegendRow("L6", activeRateLabel(6), ImVec4(0.86f, 0.22f, 0.22f, 1.0f)); + drawLegendRow("L7", activeRateLabel(7), ImVec4(0.30f, 0.30f, 0.30f, 1.0f)); + + if (auto* debugSRV = nvVrs.GetDebugVisualizationSRV()) { + ImGui::Separator(); + ImGui::Text("SRS Debug Visualization"); + const float maxW = 800.0f; + const float aspect = static_cast(vrsState.tileHeight) / std::max(static_cast(vrsState.tileWidth), 1.0f); + const float displayW = std::min(static_cast(vrsState.tileWidth) * 4.0f, maxW); + const float displayH = displayW * aspect; + ImGui::Image(reinterpret_cast(debugSRV), + ImVec2(displayW, displayH)); + } + ImGui::TreePop(); + } +} + +void VRS::SaveSettings(json& o_json) +{ + o_json = settings; + subrectCtrl.SaveSettings(o_json); +} + +void VRS::LoadSettings(json& o_json) +{ + settings = o_json; + subrectCtrl.LoadSettings(o_json); + + if (settings.vrEnableVRS > 1) { + logger::warn("[VRS] Loaded vrEnableVRS {} out of range, clamping to 1", settings.vrEnableVRS); + settings.vrEnableVRS = 1; + } + if (settings.vrVRSSrsPreset > 2) { + logger::warn("[VRS] Loaded vrVRSSrsPreset {} out of range, clamping to 2", settings.vrVRSSrsPreset); + settings.vrVRSSrsPreset = 2; + } + if (settings.vrVRSLutPreset > 2) { + logger::warn("[VRS] Loaded vrVRSLutPreset {} out of range, clamping to 2", settings.vrVRSLutPreset); + settings.vrVRSLutPreset = 2; + } + if (settings.vrVRSRingGrowthRate < 0.05f || settings.vrVRSRingGrowthRate > 1.0f) { + logger::warn("[VRS] Loaded vrVRSRingGrowthRate {} out of range, clamping", settings.vrVRSRingGrowthRate); + settings.vrVRSRingGrowthRate = std::clamp(settings.vrVRSRingGrowthRate, 0.05f, 1.0f); + } +} + +void VRS::RestoreDefaultSettings() +{ + settings = {}; + subrectCtrl = Subrect::Controller{}; +} + +void VRS::UpdateVRShadingRateState() +{ + NvVrsController::Settings vrsSettings{}; + vrsSettings.enable = settings.vrEnableVRS != 0; + vrsSettings.srsPreset = settings.vrVRSSrsPreset; + vrsSettings.lutPreset = settings.vrVRSLutPreset; + vrsSettings.ringGrowthRate = settings.vrVRSRingGrowthRate; + vrsSettings.enableDirectionalRates = settings.vrEnableDirectionalRates; + vrsSettings.enableBoundaryDither = settings.vrEnableBoundaryDither; + vrsSettings.enableDiagnostics = settings.vrEnableDiagnostics; + + { + const auto& leftUV = subrectCtrl.GetLeftEyeUV(); + const auto& rightUV = subrectCtrl.GetRightEyeUV(); + vrsSettings.leftSubrectUV = { leftUV.x, leftUV.y, leftUV.w, leftUV.h }; + vrsSettings.rightSubrectUV = { rightUV.x, rightUV.y, rightUV.w, rightUV.h }; + } + + auto screenSize = globals::state->screenSize; + auto& scale = globals::features::upscaling.resolutionScale; + + NvVrsController::FrameInfo frameInfo{}; + frameInfo.displayWidth = static_cast(screenSize.x); + frameInfo.displayHeight = static_cast(screenSize.y); + frameInfo.renderWidth = static_cast(screenSize.x * scale.x); + frameInfo.renderHeight = static_cast(screenSize.y * scale.y); + + nvVrs.Update(vrsSettings, frameInfo, globals::d3d::device, globals::d3d::context); +} + +void VRS::DisableVRShadingRateState() +{ + nvVrs.SetLastDisableReason(NvVrsController::DisableReason::UIPass); + nvVrs.Disable(globals::d3d::context); +} + +void VRS::SuspendVRS() +{ + nvVrs.SetLastDisableReason(NvVrsController::DisableReason::TerrainBlending); + nvVrs.Suspend(globals::d3d::context); +} + +void VRS::ResumeVRS() +{ + nvVrs.Resume(globals::d3d::context); +} diff --git a/src/Features/VRS.h b/src/Features/VRS.h new file mode 100644 index 0000000000..eb696ad9df --- /dev/null +++ b/src/Features/VRS.h @@ -0,0 +1,103 @@ +#pragma once + +#include "Feature.h" +#include "Utils/Subrect/Subrect.h" +#include "VRS/NvVrsController.h" + +namespace RE::BSGraphics +{ + class BSShaderAccumulator; +} + +/// NVAPI Variable Rate Shading — foveated rendering for VR. +/// +/// Uses elliptical concentric rings matched to human peripheral acuity falloff, +/// with directional-adaptive asymmetric rates (2×1/1×2) for perceptually +/// smoother transitions. Significant FPS gain on lower-end GPUs where pixel +/// shading is the bottleneck. +/// +/// Subrect integration: foveal center defined by Subrect::Controller, sharing +/// crop presets with Screenshot, DLSSEnhancer, and lossless recording (WIP). +/// +/// Terrain Blending compatibility: when TB is active, VRS is temporarily +/// suspended during RenderTerrainBlendingPasses() so terrain always shades +/// at full 1x1 rate. When TB is off, terrain benefits from reduced shading +/// rates like all other geometry. +/// +/// Hard-gated at PostPostLoad: hooks only installed in VR runtime. +struct VRS : Feature +{ +public: + virtual inline std::string GetName() override { return "Variable Rate Shading"; } + virtual inline std::string GetShortName() override { return "VRS"; } + virtual inline bool SupportsVR() override { return true; } + virtual inline bool IsCore() const override { return false; } + virtual inline std::string_view GetCategory() const override { return FeatureCategories::kDisplay; } + virtual inline bool IsInMenu() const override { return hardwareAvailable_; } + + virtual std::pair> GetFeatureSummary() override + { + return { + "NVAPI Variable Rate Shading for VR performance optimization", + { "Foveated rendering with configurable radii", + "Full 2x2 and 4x4 uniform shading rate modes", + "Per-frame diagnostics and debug state" } + }; + } + + struct Settings + { + /// 0 = Disabled, 1 = Enabled. When off, controller unbinds surface and sends 1×1 LUT. + uint vrEnableVRS = 1; + + /// SRS ring rate preset: 0=Default (6-step), 1=Faster (4-step), 2=Extreme (3-step). + uint vrVRSSrsPreset = 0; + + /// LUT debug override: 0=Default (1:1 mapping), 1=Full 1×1, 2=Full 4×4. + uint vrVRSLutPreset = 0; + + /// Ring boundary growth per step as fraction of base ellipse. + /// 0.20 = radii at 1.0×, 1.2×, 1.4× ... Clamped to [0.05, 1.0]. + float vrVRSRingGrowthRate = 0.25f; + + /// Direction-adaptive asymmetric rates (2×1/1×2, 4×2/2×4) for Default preset. + bool vrEnableDirectionalRates = true; + + /// Checkerboard dithering at ring boundaries to soften transitions. + bool vrEnableBoundaryDither = true; + + /// Enable diagnostics panel: tile stats, debug visualization (~80 KB upload per rebuild). + bool vrEnableDiagnostics = false; + }; + + Settings settings; + + virtual void DrawSettings() override; + virtual void SaveSettings(json& o_json) override; + virtual void LoadSettings(json& o_json) override; + virtual void RestoreDefaultSettings() override; + virtual void PostPostLoad() override; + + void UpdateVRShadingRateState(); + void DisableVRShadingRateState(); + void SuspendVRS(); + void ResumeVRS(); + + NvVrsController nvVrs; ///< NVAPI surface + rate table lifecycle + Subrect::Controller subrectCtrl; ///< Per-eye crop region (foveal center) + +private: + bool hardwareAvailable_ = false; ///< true after hooks installed in PostPostLoad + + struct Main_UpdateJitter + { + static void thunk(RE::BSGraphics::State* a_state); + static inline REL::Relocation func; + }; + + struct Main_FinishAccumulatingDispatch + { + static void thunk(RE::BSGraphics::BSShaderAccumulator* shaderAccumulator, uint32_t renderFlags); + static inline REL::Relocation func; + }; +}; diff --git a/src/Features/VRS/NvVrsController.cpp b/src/Features/VRS/NvVrsController.cpp new file mode 100644 index 0000000000..932d2f9510 --- /dev/null +++ b/src/Features/VRS/NvVrsController.cpp @@ -0,0 +1,425 @@ +#include "NvVrsController.h" + +#include "VrsLutManager.h" + +#include +#include +#include + +#include "Globals.h" + +namespace +{ + constexpr uint32_t kTileWidth = NV_VARIABLE_PIXEL_SHADING_TILE_WIDTH; + constexpr uint32_t kTileHeight = NV_VARIABLE_PIXEL_SHADING_TILE_HEIGHT; + + inline uint32_t CeilDiv(uint32_t v, uint32_t d) + { + return (v + d - 1u) / d; + } +} + +NvVrsController::~NvVrsController() +{ + ReleaseResources(); + if (debugState.initialized) { + NvAPI_Unload(); + } +} + +/// Per-frame entry point: guards → lazy-init → surface → pattern → bind. +void NvVrsController::Update(const Settings& settings, const FrameInfo& frameInfo, ID3D11Device* device, ID3D11DeviceContext* context) +{ + currentSettings = settings; + currentFrame = frameInfo; + + if (currentSettings.lutPreset != lastLutPreset) { + rateTableDirty = true; + lastLutPreset = currentSettings.lutPreset; + } + + if (!settings.enable) { + SetLastDisableReason(DisableReason::SettingsDisabled); + Disable(context); + return; + } + + if (!device || !context) { + SetLastDisableReason(DisableReason::InvalidContext); + Disable(context); + return; + } + + if (!EnsureInitialized(device)) { + SetLastDisableReason(DisableReason::InitFailed); + Disable(context); + return; + } + + if (!EnsureSurface(device)) { + SetLastDisableReason(DisableReason::SurfaceFailed); + Disable(context); + return; + } + + UpdateSurfaceData(context); + + // Keep official-style enable order for SRS path: bind SRRV first, then enable viewport shading rates. + if ((!debugState.active || surfaceDirty) && !BindSurface(context)) { + SetLastDisableReason(DisableReason::BindSurfaceFailed); + Disable(context); + return; + } + + if ((!debugState.active || rateTableDirty) && !BindRateTable(context)) { + SetLastDisableReason(DisableReason::BindRateTableFailed); + Disable(context); + return; + } + + debugState.active = true; + surfaceDirty = false; + rateTableDirty = false; +} + +/// Temporarily unbind VRS without dirtying state. Resume() re-binds cheaply. +void NvVrsController::Suspend(ID3D11DeviceContext* context) +{ + if (!context || !debugState.active) + return; + + NvAPI_D3D11_RSSetShadingRateResourceView(context, nullptr); + + const uint32_t viewportCount = QueryViewportCount(context, 2); + std::vector vsrd(viewportCount); + for (auto& desc : vsrd) { + desc.enableVariablePixelShadingRate = false; + VrsLutManager::FillDisabledRateTable(desc.shadingRateTable, static_cast(std::size(desc.shadingRateTable))); + } + + NV_D3D11_VIEWPORTS_SHADING_RATE_DESC srd{}; + srd.version = NV_D3D11_VIEWPORTS_SHADING_RATE_DESC_VER; + srd.numViewports = viewportCount; + srd.pViewports = vsrd.data(); + + NvAPI_D3D11_RSSetViewportsPixelShadingRates(context, &srd); + debugState.active = false; + // Note: surfaceDirty / rateTableDirty intentionally NOT set. +} + +/// Re-bind existing shading rate surface + rate table after Suspend(). +void NvVrsController::Resume(ID3D11DeviceContext* context) +{ + if (!context || debugState.active || !debugState.initialized || !debugState.supported) + return; + + if (!shadingRateSurface || !shadingRateResourceView) + return; + + if (!BindSurface(context) || !BindRateTable(context)) { + Disable(context); + return; + } + debugState.active = true; +} + +/// Unbind VRS: clear SRRV, reset viewports to 1×1, mark dirty for next Update. +void NvVrsController::Disable(ID3D11DeviceContext* context) +{ + if (!context || !debugState.initialized) { + debugState.active = false; + return; + } + + if (!debugState.active) { + return; + } + + NvAPI_D3D11_RSSetShadingRateResourceView(context, nullptr); + + const uint32_t viewportCount = QueryViewportCount(context, 2); + std::vector vsrd(viewportCount); + for (auto& desc : vsrd) { + desc.enableVariablePixelShadingRate = false; + VrsLutManager::FillDisabledRateTable(desc.shadingRateTable, static_cast(std::size(desc.shadingRateTable))); + } + + NV_D3D11_VIEWPORTS_SHADING_RATE_DESC srd{}; + srd.version = NV_D3D11_VIEWPORTS_SHADING_RATE_DESC_VER; + srd.numViewports = viewportCount; + srd.pViewports = vsrd.data(); + + auto status = NvAPI_D3D11_RSSetViewportsPixelShadingRates(context, &srd); + if (status != NVAPI_OK) { + MarkError(static_cast(status), "Disable.RSSetViewportsPixelShadingRates.Dynamic"); + } + debugState.lastViewportCount = viewportCount; + + debugState.active = false; + surfaceDirty = true; + rateTableDirty = true; +} + +/// Lazy-init NvAPI and check VPS hardware capability (called once). +bool NvVrsController::EnsureInitialized(ID3D11Device* device) +{ + if (debugState.initialized) { + return debugState.supported; + } + + auto initStatus = NvAPI_Initialize(); + if (initStatus != NVAPI_OK) { + MarkError(static_cast(initStatus), "EnsureInitialized.NvAPI_Initialize"); + debugState.initialized = false; + debugState.supported = false; + return false; + } + + NV_D3D1x_GRAPHICS_CAPS caps{}; + const NvAPI_Status graphicsCapsStatus = NvAPI_D3D1x_GetGraphicsCapabilities(device, NV_D3D1x_GRAPHICS_CAPS_VER, &caps); + if (graphicsCapsStatus != NVAPI_OK) { + MarkError(static_cast(graphicsCapsStatus), "EnsureInitialized.GetGraphicsCapabilities"); + debugState.initialized = true; + debugState.supported = false; + return false; + } + + if (!caps.bVariablePixelRateShadingSupported) { + debugState.initialized = true; + debugState.supported = false; + debugState.lastNvapiStatus = NVAPI_OK; + return false; + } + + debugState.initialized = true; + debugState.supported = true; + debugState.lastNvapiStatus = NVAPI_OK; + return true; +} + +/// (Re)create R8_UINT shading rate surface + SRRV + debug texture when resolution changes. +bool NvVrsController::EnsureSurface(ID3D11Device* device) +{ + const uint32_t surfaceWidth = CeilDiv(static_cast(std::max(currentFrame.displayWidth, 1)), kTileWidth); + const uint32_t surfaceHeight = CeilDiv(static_cast(std::max(currentFrame.displayHeight, 1)), kTileHeight); + + debugState.tileWidth = surfaceWidth; + debugState.tileHeight = surfaceHeight; + + if (shadingRateSurface && surfaceWidth == lastSurfaceWidth && surfaceHeight == lastSurfaceHeight) { + return true; + } + + debugState.lastRebuildReason = shadingRateSurface ? RebuildReason::ResolutionChanged : RebuildReason::FirstCreate; + + shadingRateSurface.Reset(); + if (shadingRateResourceView) { + shadingRateResourceView->Release(); + shadingRateResourceView = nullptr; + } + + D3D11_TEXTURE2D_DESC texDesc{}; + texDesc.Width = surfaceWidth; + texDesc.Height = surfaceHeight; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = DXGI_FORMAT_R8_UINT; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + auto hr = device->CreateTexture2D(&texDesc, nullptr, shadingRateSurface.ReleaseAndGetAddressOf()); + if (FAILED(hr) || !shadingRateSurface) { + MarkError(static_cast(hr), "EnsureSurface.CreateTexture2D"); + return false; + } + + NV_D3D11_SHADING_RATE_RESOURCE_VIEW_DESC srrvDesc{}; + srrvDesc.version = NV_D3D11_SHADING_RATE_RESOURCE_VIEW_DESC_VER; + srrvDesc.Format = DXGI_FORMAT_R8_UINT; + srrvDesc.ViewDimension = NV_SRRV_DIMENSION_TEXTURE2D; + srrvDesc.Texture2D.MipSlice = 0; + + auto status = NvAPI_D3D11_CreateShadingRateResourceView(device, shadingRateSurface.Get(), &srrvDesc, &shadingRateResourceView); + if (status != NVAPI_OK || !shadingRateResourceView) { + MarkError(static_cast(status), "EnsureSurface.CreateShadingRateResourceView"); + return false; + } + + lastSurfaceWidth = surfaceWidth; + lastSurfaceHeight = surfaceHeight; + surfaceDirty = true; + rateTableDirty = true; + + // Create debug visualisation texture (R8G8B8A8_UNORM, same tile dimensions). + debugVisualizationTexture.Reset(); + debugVisualizationSRV.Reset(); + { + D3D11_TEXTURE2D_DESC dbgDesc{}; + dbgDesc.Width = surfaceWidth; + dbgDesc.Height = surfaceHeight; + dbgDesc.MipLevels = 1; + dbgDesc.ArraySize = 1; + dbgDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + dbgDesc.SampleDesc.Count = 1; + dbgDesc.Usage = D3D11_USAGE_DEFAULT; + dbgDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + auto dbgHr = device->CreateTexture2D(&dbgDesc, nullptr, debugVisualizationTexture.ReleaseAndGetAddressOf()); + if (SUCCEEDED(dbgHr) && debugVisualizationTexture) { + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + auto srvHr = device->CreateShaderResourceView(debugVisualizationTexture.Get(), &srvDesc, debugVisualizationSRV.ReleaseAndGetAddressOf()); + if (FAILED(srvHr) || !debugVisualizationSRV) { + debugVisualizationTexture.Reset(); + debugVisualizationSRV.Reset(); + } + } + } + srsBuilder = VrsSrsBuilder{}; + + return true; +} + +/// Rebuild SRS pattern if dirty/changed, then upload to GPU via UpdateSubresource. +void NvVrsController::UpdateSurfaceData(ID3D11DeviceContext* context) +{ + const uint32_t width = lastSurfaceWidth; + const uint32_t height = lastSurfaceHeight; + const uint32_t renderWidth = CeilDiv(static_cast(std::max(currentFrame.renderWidth, 1)), kTileWidth); + const uint32_t renderHeight = CeilDiv(static_cast(std::max(currentFrame.renderHeight, 1)), kTileHeight); + + if (patternBuffer.size() != static_cast(width) * static_cast(height)) { + patternBuffer.resize(static_cast(width) * static_cast(height)); + } + + VrsSrsBuilder::Params srsParams; + srsParams.leftSubrectUV = currentSettings.leftSubrectUV; + srsParams.rightSubrectUV = currentSettings.rightSubrectUV; + srsParams.ringGrowthRate = currentSettings.ringGrowthRate; + srsParams.srsPreset = currentSettings.srsPreset; + srsParams.enableDirectionalRates = currentSettings.enableDirectionalRates; + srsParams.enableBoundaryDither = currentSettings.enableBoundaryDither; + + if (!surfaceDirty && !srsBuilder.NeedsRebuild(width, height, renderWidth, renderHeight, srsParams)) { + debugState.patternReuseCount++; + return; + } + + srsBuilder.Build(patternBuffer.data(), width, height, renderWidth, renderHeight, srsParams); + debugState.patternRebuildCount++; + + if (currentSettings.enableDiagnostics) { + std::fill(std::begin(debugState.lastTileLevelCount), std::end(debugState.lastTileLevelCount), 0ull); + for (size_t i = 0; i < patternBuffer.size(); ++i) { + const uint8_t level = patternBuffer[i]; + if (level < SrsLevel::kCount) { + debugState.lastTileLevelCount[level]++; + } + } + + // Build debug visualisation into a separate RGBA buffer. + if (debugVisualizationBuffer.size() != static_cast(width) * height) { + debugVisualizationBuffer.resize(static_cast(width) * height); + } + VrsSrsBuilder::BuildDebugVisualization(debugVisualizationBuffer.data(), patternBuffer.data(), width, height); + } + + // rowPitch uses bytes per row; for R8_UINT each tile is 1 byte, so rowPitch == width. + context->UpdateSubresource(shadingRateSurface.Get(), 0, nullptr, patternBuffer.data(), width, 0); + + // Upload debug visualisation (RGBA, 4 bytes per tile). + if (currentSettings.enableDiagnostics && debugVisualizationTexture) { + context->UpdateSubresource(debugVisualizationTexture.Get(), 0, nullptr, + debugVisualizationBuffer.data(), width * sizeof(uint32_t), 0); + } + + srsBuilder.UpdateCache(width, height, renderWidth, renderHeight, srsParams); +} + +bool NvVrsController::BindRateTable(ID3D11DeviceContext* context) +{ + const uint32_t viewportCount = QueryViewportCount(context, 2); + std::vector vsrd(viewportCount); + for (auto& desc : vsrd) { + desc.enableVariablePixelShadingRate = true; + VrsLutManager::FillActiveRateTable(desc.shadingRateTable, static_cast(std::size(desc.shadingRateTable)), currentSettings.lutPreset); + } + + NV_D3D11_VIEWPORTS_SHADING_RATE_DESC srd{}; + srd.version = NV_D3D11_VIEWPORTS_SHADING_RATE_DESC_VER; + srd.numViewports = viewportCount; + srd.pViewports = vsrd.data(); + + auto status = NvAPI_D3D11_RSSetViewportsPixelShadingRates(context, &srd); + if (status != NVAPI_OK) { + MarkError(static_cast(status), "BindRateTable.RSSetViewportsPixelShadingRates"); + return false; + } + + debugState.lastNvapiStatus = NVAPI_OK; + debugState.lastViewportCount = viewportCount; + return true; +} + +bool NvVrsController::BindSurface(ID3D11DeviceContext* context) +{ + auto status = NvAPI_D3D11_RSSetShadingRateResourceView(context, shadingRateResourceView); + if (status != NVAPI_OK) { + MarkError(static_cast(status), "BindSurface.RSSetShadingRateResourceView"); + return false; + } + + debugState.lastNvapiStatus = NVAPI_OK; + return true; +} + +void NvVrsController::ReleaseResources() +{ + if (shadingRateResourceView) { + shadingRateResourceView->Release(); + shadingRateResourceView = nullptr; + } + shadingRateSurface.Reset(); + lastSurfaceWidth = 0; + lastSurfaceHeight = 0; + patternBuffer.clear(); + debugVisualizationTexture.Reset(); + debugVisualizationSRV.Reset(); + debugVisualizationBuffer.clear(); +} + +void NvVrsController::MarkError(int status, const char* site) +{ + debugState.lastNvapiStatus = status; + debugState.lastFailureSite = site; + debugState.failureCount++; +} + +uint32_t NvVrsController::QueryViewportCount(ID3D11DeviceContext* context, uint32_t fallback) const +{ + if (!context) { + return fallback; + } + + UINT viewportCount = 0; + context->RSGetViewports(&viewportCount, nullptr); + if (viewportCount == 0) { + return fallback; + } + + return static_cast(viewportCount); +} + +ID3D11ShaderResourceView* NvVrsController::GetDebugVisualizationSRV() const +{ + return debugVisualizationSRV.Get(); +} + +void NvVrsController::SetLastDisableReason(DisableReason reason) +{ + if (reason != DisableReason::None) { + debugState.lastDisableReason = reason; + } +} diff --git a/src/Features/VRS/NvVrsController.h b/src/Features/VRS/NvVrsController.h new file mode 100644 index 0000000000..99fdc27699 --- /dev/null +++ b/src/Features/VRS/NvVrsController.h @@ -0,0 +1,123 @@ +#pragma once + +#include "VrsLutManager.h" +#include "VrsSrsBuilder.h" + +#include +#include +#include +#include + +#include + +/// NvVrsController — manages the NVAPI VRS pipeline for DX11. +class NvVrsController +{ +public: + /// Reason VRS was disabled this frame (sticky for diagnostics). + enum class DisableReason : uint32_t + { + None = 0, + SettingsDisabled, + InvalidContext, + InitFailed, + SurfaceFailed, + BindSurfaceFailed, + BindRateTableFailed, + UIPass, + TerrainBlending, + }; + + enum class RebuildReason : uint32_t + { + None = 0, + FirstCreate, + ResolutionChanged, + }; + + /// Per-frame parameters assembled by VRS::UpdateVRShadingRateState. + struct Settings + { + bool enable = false; + uint32_t srsPreset = 0; // 0=Default, 1=Faster, 2=Extreme + uint32_t lutPreset = 0; // 0=Default, 1=Full 1×1, 2=Full 4×4 + VrsSrsBuilder::EyeSubrectUV leftSubrectUV{}; + VrsSrsBuilder::EyeSubrectUV rightSubrectUV{}; + float ringGrowthRate = 0.25f; + bool enableDirectionalRates = true; + bool enableBoundaryDither = true; + bool enableDiagnostics = false; + }; + + /// display = backbuffer size (tile grid basis), render = after DRS scaling. + struct FrameInfo + { + int displayWidth = 0; + int displayHeight = 0; + int renderWidth = 0; + int renderHeight = 0; + }; + + /// Diagnostics snapshot exposed to the settings UI. + struct DebugState + { + bool initialized = false; + bool supported = false; + bool active = false; + int lastNvapiStatus = 0; + uint32_t tileWidth = 0; + uint32_t tileHeight = 0; + uint64_t patternRebuildCount = 0; + uint64_t patternReuseCount = 0; + + uint64_t lastTileLevelCount[SrsLevel::kCount] = {}; + uint32_t lastViewportCount = 0; + DisableReason lastDisableReason = DisableReason::None; + RebuildReason lastRebuildReason = RebuildReason::None; + uint64_t failureCount = 0; + const char* lastFailureSite = "None"; + }; + + NvVrsController() = default; + ~NvVrsController(); + + void Update(const Settings& settings, const FrameInfo& frameInfo, ID3D11Device* device, ID3D11DeviceContext* context); + void Disable(ID3D11DeviceContext* context); + void Suspend(ID3D11DeviceContext* context); + void Resume(ID3D11DeviceContext* context); + void SetLastDisableReason(DisableReason reason); + + const DebugState& GetDebugState() const { return debugState; } + ID3D11ShaderResourceView* GetDebugVisualizationSRV() const; + +private: + DebugState debugState{}; + + Settings currentSettings{}; + FrameInfo currentFrame{}; + + Microsoft::WRL::ComPtr shadingRateSurface; + ID3D11NvShadingRateResourceView* shadingRateResourceView = nullptr; + std::vector patternBuffer; + + Microsoft::WRL::ComPtr debugVisualizationTexture; + Microsoft::WRL::ComPtr debugVisualizationSRV; + std::vector debugVisualizationBuffer; + + VrsSrsBuilder srsBuilder; + + uint32_t lastSurfaceWidth = 0; + uint32_t lastSurfaceHeight = 0; + bool surfaceDirty = false; + bool rateTableDirty = true; + uint32_t lastLutPreset = UINT32_MAX; // force first-frame rebind + bool EnsureInitialized(ID3D11Device* device); + bool EnsureSurface(ID3D11Device* device); + void UpdateSurfaceData(ID3D11DeviceContext* context); + + bool BindRateTable(ID3D11DeviceContext* context); + bool BindSurface(ID3D11DeviceContext* context); + uint32_t QueryViewportCount(ID3D11DeviceContext* context, uint32_t fallback) const; + void ReleaseResources(); + void MarkError(int status, const char* site); +}; diff --git a/src/Features/VRS/VrsLutManager.cpp b/src/Features/VRS/VrsLutManager.cpp new file mode 100644 index 0000000000..7966541e65 --- /dev/null +++ b/src/Features/VRS/VrsLutManager.cpp @@ -0,0 +1,42 @@ +#include "VrsLutManager.h" + +#include "VrsSrsBuilder.h" +#include + +void VrsLutManager::FillActiveRateTable(NV_PIXEL_SHADING_RATE* table, uint32_t count, uint32_t lutPreset) +{ + std::fill(table, table + count, NV_PIXEL_X1_PER_RASTER_PIXEL); + + switch (lutPreset) { + case 1: // Full 1×1: force native rate (debug) + break; + case 2: // Full 4×4: force coarsest rate (debug) + std::fill(table, table + count, NV_PIXEL_X1_PER_4X4_RASTER_PIXELS); + if (count > SrsLevel::kCull) + table[SrsLevel::kCull] = NV_PIXEL_X0_CULL_RASTER_PIXELS; + break; + default: // Default: 1:1 mapping preserving directional asymmetry + if (count > SrsLevel::k1x1) + table[SrsLevel::k1x1] = NV_PIXEL_X1_PER_RASTER_PIXEL; + if (count > SrsLevel::k2x1) + table[SrsLevel::k2x1] = NV_PIXEL_X1_PER_2X1_RASTER_PIXELS; + if (count > SrsLevel::k1x2) + table[SrsLevel::k1x2] = NV_PIXEL_X1_PER_1X2_RASTER_PIXELS; + if (count > SrsLevel::k2x2) + table[SrsLevel::k2x2] = NV_PIXEL_X1_PER_2X2_RASTER_PIXELS; + if (count > SrsLevel::k4x2) + table[SrsLevel::k4x2] = NV_PIXEL_X1_PER_4X2_RASTER_PIXELS; + if (count > SrsLevel::k2x4) + table[SrsLevel::k2x4] = NV_PIXEL_X1_PER_2X4_RASTER_PIXELS; + if (count > SrsLevel::k4x4) + table[SrsLevel::k4x4] = NV_PIXEL_X1_PER_4X4_RASTER_PIXELS; + if (count > SrsLevel::kCull) + table[SrsLevel::kCull] = NV_PIXEL_X0_CULL_RASTER_PIXELS; + break; + } +} + +void VrsLutManager::FillDisabledRateTable(NV_PIXEL_SHADING_RATE* table, uint32_t count) +{ + std::fill(table, table + count, NV_PIXEL_X1_PER_RASTER_PIXEL); +} diff --git a/src/Features/VRS/VrsLutManager.h b/src/Features/VRS/VrsLutManager.h new file mode 100644 index 0000000000..895e5ceb70 --- /dev/null +++ b/src/Features/VRS/VrsLutManager.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +/// LUT manager for NVAPI per-viewport shading rates. +/// Default preset: 1:1 mapping preserving directional asymmetry. +/// Debug presets override all entries to a uniform rate. +class VrsLutManager +{ +public: + static void FillActiveRateTable(NV_PIXEL_SHADING_RATE* table, uint32_t count, uint32_t lutPreset = 0); + static void FillDisabledRateTable(NV_PIXEL_SHADING_RATE* table, uint32_t count); +}; diff --git a/src/Features/VRS/VrsSrsBuilder.cpp b/src/Features/VRS/VrsSrsBuilder.cpp new file mode 100644 index 0000000000..82c6e17b28 --- /dev/null +++ b/src/Features/VRS/VrsSrsBuilder.cpp @@ -0,0 +1,230 @@ +#include "VrsSrsBuilder.h" + +#include +#include +#include + +namespace +{ + // Ring rate sequences per preset. Each defines the shading rate + // progression from foveal center outward; last entry repeats for + // all rings beyond array length. + + // Default: 6-step gradual with directional half/eighth rates. + static constexpr RingRate kDefaultSeq[] = { + RingRate::Rate_1x1, + RingRate::Rate_Half, + RingRate::Rate_2x2, + RingRate::Rate_Eighth, + RingRate::Rate_4x4, + RingRate::Rate_Cull, + }; + + // Faster: 4-step symmetric, no directional split. + static constexpr RingRate kFasterSeq[] = { + RingRate::Rate_1x1, + RingRate::Rate_2x2, + RingRate::Rate_4x4, + RingRate::Rate_Cull, + }; + + // Extreme: 3-step, native → coarsest → cull. + static constexpr RingRate kExtremeSeq[] = { + RingRate::Rate_1x1, + RingRate::Rate_4x4, + RingRate::Rate_Cull, + }; + + // Boundary dither: checkerboard pattern at ring boundaries to soften transitions. + constexpr float kDitherFraction = 0.15f; + + // Debug visualisation: RGBA colour per SrsLevel. + constexpr uint32_t kDebugColors[SrsLevel::kCount] = { + 0xFF59D933u, // k1x1: green + 0xFFD9BF33u, // k2x1: cyan + 0xFFBFD933u, // k1x2: teal + 0xFF33CCF2u, // k2x2: yellow + 0xFF268CFAu, // k4x2: orange + 0xFF4D73F2u, // k2x4: salmon + 0xFF3838DBu, // k4x4: red + 0xFF4D4D4Du, // kCull: dark grey + }; +} + +// --- Static helpers --- + +void VrsSrsBuilder::GetRingRateSequence(uint32_t preset, const RingRate*& out, uint32_t& count) +{ + switch (preset) { + case 1: + out = kFasterSeq; + count = static_cast(std::size(kFasterSeq)); + break; + case 2: + out = kExtremeSeq; + count = static_cast(std::size(kExtremeSeq)); + break; + case 0: + default: + out = kDefaultSeq; + count = static_cast(std::size(kDefaultSeq)); + break; + } +} + +uint8_t VrsSrsBuilder::ResolveRingLevel(RingRate rate, float dx, float dy, float halfW, float halfH, bool directional) +{ + switch (rate) { + case RingRate::Rate_1x1: + return SrsLevel::k1x1; + case RingRate::Rate_Half: + if (directional) { + // Directional half-rate: tiles near horizontal axis use 2×1 (coarsen + // vertically, preserve the horizontal detail human eyes track along); + // tiles near vertical axis use 1×2 (opposite). + const bool horizontal = std::abs(dy) * halfW < std::abs(dx) * halfH; + return horizontal ? SrsLevel::k2x1 : SrsLevel::k1x2; + } + return SrsLevel::k2x1; + case RingRate::Rate_2x2: + return SrsLevel::k2x2; + case RingRate::Rate_Eighth: + if (directional) { + // Same directional logic at eighth-rate: 4×2 horizontal / 2×4 vertical. + const bool horizontal = std::abs(dy) * halfW < std::abs(dx) * halfH; + return horizontal ? SrsLevel::k4x2 : SrsLevel::k2x4; + } + return SrsLevel::k4x2; + case RingRate::Rate_4x4: + return SrsLevel::k4x4; + case RingRate::Rate_Cull: + default: + return SrsLevel::kCull; + } +} + +void VrsSrsBuilder::BuildDebugVisualization(uint32_t* dst, const uint8_t* src, uint32_t width, uint32_t height) +{ + const uint32_t count = width * height; + for (uint32_t i = 0; i < count; ++i) { + const uint8_t level = src[i]; + dst[i] = (level < SrsLevel::kCount) ? kDebugColors[level] : kDebugColors[SrsLevel::kCull]; + } +} + +// --- NeedsRebuild --- + +bool VrsSrsBuilder::NeedsRebuild(uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params) const +{ + if (!hasCache) + return true; + if (width != lastWidth || height != lastHeight || renderWidth != lastRenderWidth || renderHeight != lastRenderHeight) + return true; + if (params.srsPreset != lastParams.srsPreset) + return true; + if (params.enableDirectionalRates != lastParams.enableDirectionalRates || + params.enableBoundaryDither != lastParams.enableBoundaryDither) + return true; + if (params.leftSubrectUV.x != lastParams.leftSubrectUV.x || + params.leftSubrectUV.y != lastParams.leftSubrectUV.y || + params.leftSubrectUV.w != lastParams.leftSubrectUV.w || + params.leftSubrectUV.h != lastParams.leftSubrectUV.h || + params.rightSubrectUV.x != lastParams.rightSubrectUV.x || + params.rightSubrectUV.y != lastParams.rightSubrectUV.y || + params.rightSubrectUV.w != lastParams.rightSubrectUV.w || + params.rightSubrectUV.h != lastParams.rightSubrectUV.h || + params.ringGrowthRate != lastParams.ringGrowthRate) + return true; + return false; +} + +// --- Build --- + +void VrsSrsBuilder::Build(uint8_t* dst, uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params) const +{ + const RingRate* seq = nullptr; + uint32_t seqLen = 0; + GetRingRateSequence(params.srsPreset, seq, seqLen); + + const uint32_t eyeRenderWidth = renderWidth / 2u; + const float ringStep = std::max(0.01f, params.ringGrowthRate); + + // Pre-fill: kCull for tiles outside renderRes (never shaded during world pass). + // VRS is disabled before UI/Post passes via the FinishAccumulatingDispatch hook, + // so the cull pre-fill only applies during the world render pass. + std::memset(dst, SrsLevel::kCull, static_cast(width) * height); + + // Per-eye: compute concentric elliptical zones from subrect UV. + const EyeSubrectUV* eyeUVs[2] = { ¶ms.leftSubrectUV, ¶ms.rightSubrectUV }; + for (uint32_t eye = 0; eye < 2; ++eye) { + const uint32_t eyeOffsetX = eye * eyeRenderWidth; + const auto& uv = *eyeUVs[eye]; + + // Ellipse center in tile coordinates. + const float cx = static_cast(eyeOffsetX) + (uv.x + uv.w * 0.5f) * static_cast(eyeRenderWidth); + const float cy = (uv.y + uv.h * 0.5f) * static_cast(renderHeight); + + // Subrect half-dimensions in tiles (for directional angle detection). + const float halfW = uv.w * 0.5f * static_cast(eyeRenderWidth); + const float halfH = uv.h * 0.5f * static_cast(renderHeight); + + // Semi-axes of the circumscribed ellipse (sqrt2 * subrect half-dims). + constexpr float kSqrt2 = 1.41421356f; + const float a = halfW * kSqrt2; + const float b = halfH * kSqrt2; + + if (a < 0.5f || b < 0.5f) + continue; + + const uint32_t eyeMaxX = std::min(eyeOffsetX + eyeRenderWidth, renderWidth); + for (uint32_t ty = 0; ty < renderHeight && ty < height; ++ty) { + for (uint32_t tx = eyeOffsetX; tx < eyeMaxX && tx < width; ++tx) { + const uint32_t idx = ty * width + tx; + + const float dxRaw = static_cast(tx) + 0.5f - cx; + const float dyRaw = static_cast(ty) + 0.5f - cy; + const float dxNorm = dxRaw / a; + const float dyNorm = dyRaw / b; + const float d = std::sqrt(dxNorm * dxNorm + dyNorm * dyNorm); + + // Determine ring index: ring 0 at d<=1.0, ring n at 1+n*step. + uint32_t ringIdx; + if (d <= 1.0f) { + ringIdx = 0; + } else { + const float outer = (d - 1.0f) / ringStep; + ringIdx = 1u + static_cast(outer); + + // Boundary dithering: tiles just past a ring boundary + // adopt the inner ring's rate in a checkerboard pattern, + // expanding the better-quality zone outward. + // Never dither the cull boundary — hard cull outside outer ring. + if (params.enableBoundaryDither && ringIdx < seqLen - 1) { + const float frac = outer - std::floor(outer); + if (frac < kDitherFraction && ((tx + ty) & 1u)) { + ringIdx--; + } + } + } + + // Clamp to sequence bounds. + if (ringIdx >= seqLen) + ringIdx = seqLen - 1; + + dst[idx] = ResolveRingLevel( + seq[ringIdx], dxRaw, dyRaw, halfW, halfH, + params.enableDirectionalRates); + } + } + } +} + +void VrsSrsBuilder::UpdateCache(uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params) +{ + lastWidth = width; + lastHeight = height; + lastRenderWidth = renderWidth; + lastRenderHeight = renderHeight; + lastParams = params; + hasCache = true; +} diff --git a/src/Features/VRS/VrsSrsBuilder.h b/src/Features/VRS/VrsSrsBuilder.h new file mode 100644 index 0000000000..478504c956 --- /dev/null +++ b/src/Features/VRS/VrsSrsBuilder.h @@ -0,0 +1,83 @@ +#pragma once + +#include + +/// Shading rate level constants — indices into the NVAPI per-viewport LUT. +/// Higher value = coarser shading rate. Stored per-tile in the SRS R8_UINT texture. +namespace SrsLevel +{ + constexpr uint8_t k1x1 = 0; // 1x1: native rate + constexpr uint8_t k2x1 = 1; // 2x1: half rate, preserve horizontal resolution + constexpr uint8_t k1x2 = 2; // 1x2: half rate, preserve vertical resolution + constexpr uint8_t k2x2 = 3; // 2x2: quarter rate + constexpr uint8_t k4x2 = 4; // 4x2: eighth rate, preserve horizontal resolution + constexpr uint8_t k2x4 = 5; // 2x4: eighth rate, preserve vertical resolution + constexpr uint8_t k4x4 = 6; // 4x4: sixteenth rate + constexpr uint8_t kCull = 7; // CULL: skip pixel shading entirely + constexpr uint8_t kCount = 8; +} + +/// Ring rate category — determines how a ring is resolved to an SrsLevel. +/// Symmetric rates always produce the same level; directional rates choose +/// a horizontal or vertical variant based on the tile's angular position +/// relative to the subrect center diagonal. +enum class RingRate : uint8_t +{ + Rate_1x1, // -> SrsLevel::k1x1 + Rate_Half, // -> k2x1 (horizontal sector) or k1x2 (vertical sector) + Rate_2x2, // -> SrsLevel::k2x2 + Rate_Eighth, // -> k4x2 (horizontal sector) or k2x4 (vertical sector) + Rate_4x4, // -> SrsLevel::k4x4 + Rate_Cull, // -> SrsLevel::kCull +}; + +/// SRS pattern builder — purely CPU-side, generates per-tile shading rates. +/// Elliptical concentric rings with rates adapted to human peripheral acuity: +/// center = native, outer rings = progressively coarser, outside = cull. +/// Directional-adaptive rates (2×1/1×2, 4×2/2×4) align pixel coarsening +/// with the weaker perceptual axis at each angular position. +class VrsSrsBuilder +{ +public: + struct EyeSubrectUV + { + float x = 0.0f; + float y = 0.0f; + float w = 1.0f; + float h = 1.0f; + }; + + struct Params + { + uint32_t srsPreset = 0; ///< 0=Default (6-step), 1=Faster (4-step), 2=Extreme (3-step) + EyeSubrectUV leftSubrectUV{}; ///< Foveal center UV for left eye + EyeSubrectUV rightSubrectUV{}; ///< Foveal center UV for right eye + float ringGrowthRate = 0.25f; ///< Ring boundary step as fraction of base ellipse + bool enableDirectionalRates = true; ///< Asymmetric 2×1/1×2 based on tile angle + bool enableBoundaryDither = true; ///< Checkerboard dither at ring boundaries + }; + + bool NeedsRebuild(uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params) const; + + /// Fill dst with per-tile SrsLevel values for the full stereo surface. + void Build(uint8_t* dst, uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params) const; + + void UpdateCache(uint32_t width, uint32_t height, uint32_t renderWidth, uint32_t renderHeight, const Params& params); + + /// Generate an R8G8B8A8 debug visualisation from SRS tile data. + static void BuildDebugVisualization(uint32_t* dst, const uint8_t* src, uint32_t width, uint32_t height); + + /// Retrieve the ring rate sequence for a preset index. + static void GetRingRateSequence(uint32_t preset, const RingRate*& out, uint32_t& count); + + /// Resolve a RingRate into a concrete SrsLevel. + static uint8_t ResolveRingLevel(RingRate rate, float dx, float dy, float halfW, float halfH, bool directional); + +private: + uint32_t lastWidth = 0; + uint32_t lastHeight = 0; + uint32_t lastRenderWidth = 0; + uint32_t lastRenderHeight = 0; + bool hasCache = false; + Params lastParams{}; +}; diff --git a/src/Globals.cpp b/src/Globals.cpp index 221bb8c3ad..9a420a99d0 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -30,6 +30,7 @@ #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" #include "Features/VR.h" +#include "Features/VRS.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -83,6 +84,7 @@ namespace globals ExtendedTranslucency extendedTranslucency{}; Upscaling upscaling{}; HDRDisplay hdrDisplay{}; + VRS vrs{}; RenderDoc renderDoc{}; WeatherEditor weatherEditor{}; ExponentialHeightFog exponentialHeightFog{}; diff --git a/src/Globals.h b/src/Globals.h index c73b7c1e9e..7a822d10e4 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -32,6 +32,7 @@ struct PerformanceOverlay; struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; +struct VRS; struct WeatherEditor; struct ExponentialHeightFog; struct HDRDisplay; @@ -90,6 +91,7 @@ namespace globals extern ExtendedTranslucency extendedTranslucency; extern Upscaling upscaling; extern HDRDisplay hdrDisplay; + extern VRS vrs; extern RenderDoc renderDoc; extern WeatherEditor weatherEditor; extern ExponentialHeightFog exponentialHeightFog; diff --git a/src/Utils/Subrect/Subrect.cpp b/src/Utils/Subrect/Subrect.cpp new file mode 100644 index 0000000000..b2743cbd87 --- /dev/null +++ b/src/Utils/Subrect/Subrect.cpp @@ -0,0 +1,315 @@ +#include "Utils/Subrect/Subrect.h" + +#include +#include + +namespace +{ + Subrect::UVRegion ClampUV(Subrect::UVRegion uv) + { + uv.x = std::clamp(uv.x, 0.0f, 1.0f); + uv.y = std::clamp(uv.y, 0.0f, 1.0f); + uv.w = std::clamp(uv.w, 0.01f, 1.0f); + uv.h = std::clamp(uv.h, 0.01f, 1.0f); + + if (uv.x + uv.w > 1.0f) { + uv.w = 1.0f - uv.x; + } + if (uv.y + uv.h > 1.0f) { + uv.h = 1.0f - uv.y; + } + + return uv; + } + + Subrect::UVRegion DefaultUV() + { + return {}; + } + + Subrect::UVRegion LoadUVFromJson(const json& value, const char* legacyKey = nullptr) + { + Subrect::UVRegion uv = DefaultUV(); + if (value.is_array() && value.size() == 4) { + uv.x = value[0]; + uv.y = value[1]; + uv.w = value[2]; + uv.h = value[3]; + } else if (legacyKey != nullptr && value.contains(legacyKey) && value[legacyKey].is_array() && value[legacyKey].size() == 4) { + uv.x = value[legacyKey][0]; + uv.y = value[legacyKey][1]; + uv.w = value[legacyKey][2]; + uv.h = value[legacyKey][3]; + } + return ClampUV(uv); + } + + json SaveUVToJson(const Subrect::UVRegion& uv) + { + return { uv.x, uv.y, uv.w, uv.h }; + } + + Subrect::PixelRegion UVToPixelRegion(const Subrect::UVRegion& uv, uint32_t eyeWidth, uint32_t eyeHeight) + { + Subrect::PixelRegion result; + result.x = std::min(eyeWidth - 1, static_cast(uv.x * eyeWidth)); + result.y = std::min(eyeHeight - 1, static_cast(uv.y * eyeHeight)); + result.w = std::max(1, static_cast(uv.w * eyeWidth)); + result.h = std::max(1, static_cast(uv.h * eyeHeight)); + result.w = std::min(result.w, eyeWidth - result.x); + result.h = std::min(result.h, eyeHeight - result.y); + return result; + } +} + +namespace Subrect +{ + void Controller::LoadSettings(const json& a_json) + { + if (a_json.contains("CropX")) + currentLeftEyeUV.x = a_json["CropX"]; + if (a_json.contains("CropY")) + currentLeftEyeUV.y = a_json["CropY"]; + if (a_json.contains("CropW")) + currentLeftEyeUV.w = a_json["CropW"]; + if (a_json.contains("CropH")) + currentLeftEyeUV.h = a_json["CropH"]; + + if (a_json.contains("CropRightX")) + currentRightEyeUV.x = a_json["CropRightX"]; + if (a_json.contains("CropRightY")) + currentRightEyeUV.y = a_json["CropRightY"]; + if (a_json.contains("CropRightW")) + currentRightEyeUV.w = a_json["CropRightW"]; + if (a_json.contains("CropRightH")) + currentRightEyeUV.h = a_json["CropRightH"]; + + if (a_json.contains("CropPresets") && a_json["CropPresets"].is_array()) { + presets.clear(); + for (auto& entry : a_json["CropPresets"]) { + Preset preset; + preset.name = entry.value("name", "Unknown"); + if (entry.contains("left_uv")) { + preset.leftEye = LoadUVFromJson(entry["left_uv"]); + } else { + preset.leftEye = LoadUVFromJson(entry, "uv"); + } + + if (entry.contains("right_uv")) { + preset.rightEye = LoadUVFromJson(entry["right_uv"]); + } else { + preset.rightEye = preset.leftEye; + } + + presets.push_back(std::move(preset)); + } + } + + EnsureDefaultPreset(); + ClampCurrentUV(); + if (!a_json.contains("CropRightX") && !a_json.contains("CropRightY") && !a_json.contains("CropRightW") && !a_json.contains("CropRightH")) { + SyncRightEyeUV(); + } + + if (a_json.contains("SelectedPresetIndex")) { + selectedPresetIndex = a_json["SelectedPresetIndex"]; + if (selectedPresetIndex >= 0 && selectedPresetIndex < static_cast(presets.size())) { + ApplyPreset(selectedPresetIndex); + } else { + selectedPresetIndex = -1; + } + } + } + + void Controller::SaveSettings(json& a_json) const + { + a_json["CropX"] = currentLeftEyeUV.x; + a_json["CropY"] = currentLeftEyeUV.y; + a_json["CropW"] = currentLeftEyeUV.w; + a_json["CropH"] = currentLeftEyeUV.h; + a_json["CropRightX"] = currentRightEyeUV.x; + a_json["CropRightY"] = currentRightEyeUV.y; + a_json["CropRightW"] = currentRightEyeUV.w; + a_json["CropRightH"] = currentRightEyeUV.h; + + json presetsJson = json::array(); + for (const auto& preset : presets) { + json entry; + entry["name"] = preset.name; + entry["uv"] = SaveUVToJson(preset.leftEye); + entry["left_uv"] = SaveUVToJson(preset.leftEye); + entry["right_uv"] = SaveUVToJson(preset.rightEye); + presetsJson.push_back(std::move(entry)); + } + a_json["CropPresets"] = presetsJson; + a_json["SelectedPresetIndex"] = selectedPresetIndex; + } + + void Controller::DrawEditor(ID3D11ShaderResourceView* previewSrv, ID3D11Texture2D* previewTexture, float eyeRatio) + { + ImGui::Text("=== VR Capture Cropping (Left Eye Relative) ==="); + + std::string currentPreview = + (selectedPresetIndex >= 0 && selectedPresetIndex < static_cast(presets.size())) ? presets[selectedPresetIndex].name : "(Custom)"; + + if (ImGui::BeginCombo("Crop Preset", currentPreview.c_str())) { + for (int i = 0; i < static_cast(presets.size()); ++i) { + const bool isSelected = selectedPresetIndex == i; + if (ImGui::Selectable(presets[i].name.c_str(), isSelected)) { + ApplyPreset(i); + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + ImGui::InputText("Save As", newPresetName, sizeof(newPresetName)); + ImGui::SameLine(); + if (ImGui::Button("Save Preset")) { + std::string presetName = newPresetName; + if (!presetName.empty()) { + presets.push_back(Preset{ + .name = presetName, + .leftEye = currentLeftEyeUV, + .rightEye = currentRightEyeUV, + }); + selectedPresetIndex = static_cast(presets.size()) - 1; + newPresetName[0] = '\0'; + } + } + + if (selectedPresetIndex > 0) { + ImGui::SameLine(); + if (ImGui::Button("Delete Preset")) { + presets.erase(presets.begin() + selectedPresetIndex); + ApplyPreset(0); + } + } + + ImGui::Spacing(); + ImGui::PushItemWidth(250.0f); + bool changed = false; + changed |= ImGui::SliderFloat2("Position UV (X, Y)", ¤tLeftEyeUV.x, 0.0f, 1.0f, "%.3f"); + changed |= ImGui::SliderFloat2("Size UV (W, H)", ¤tLeftEyeUV.w, 0.01f, 1.0f, "%.3f"); + ImGui::PopItemWidth(); + + if (changed) { + selectedPresetIndex = -1; + ClampCurrentUV(); + SyncRightEyeUV(); + } + + ImGui::Spacing(); + ImGui::TextDisabled("Right eye UV mirrors the left-eye preset."); + ImGui::Text("Interactive Cropping (Drag on the image to select)"); + + if (!previewSrv || !previewTexture) { + ImGui::TextDisabled("Preview unavailable."); + return; + } + + D3D11_TEXTURE2D_DESC desc{}; + previewTexture->GetDesc(&desc); + float maxWidth = std::min(400.0f, ImGui::GetContentRegionAvail().x); + float aspectRatio = (static_cast(desc.Width) * eyeRatio) / static_cast(desc.Height); + ImVec2 imageSize(maxWidth, maxWidth / aspectRatio); + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + + ImGui::Image(reinterpret_cast(previewSrv), imageSize, ImVec2(0.0f, 0.0f), ImVec2(eyeRatio, 1.0f)); + + ImGui::SetCursorScreenPos(cursorPos); + ImGui::SetNextItemAllowOverlap(); + ImGui::InvisibleButton("##subrectCanvas", imageSize); + + ImVec2 mousePos = ImGui::GetIO().MousePos; + ImVec2 relativeMouseP(mousePos.x - cursorPos.x, mousePos.y - cursorPos.y); + float mouseUVX = std::clamp(relativeMouseP.x / imageSize.x, 0.0f, 1.0f); + float mouseUVY = std::clamp(relativeMouseP.y / imageSize.y, 0.0f, 1.0f); + + if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + isDraggingCrop = true; + selectedPresetIndex = -1; + dragStartUV[0] = mouseUVX; + dragStartUV[1] = mouseUVY; + currentLeftEyeUV.x = mouseUVX; + currentLeftEyeUV.y = mouseUVY; + currentLeftEyeUV.w = 0.0f; + currentLeftEyeUV.h = 0.0f; + } + + if (isDraggingCrop) { + float minX = std::min(dragStartUV[0], mouseUVX); + float minY = std::min(dragStartUV[1], mouseUVY); + float maxX = std::max(dragStartUV[0], mouseUVX); + float maxY = std::max(dragStartUV[1], mouseUVY); + + currentLeftEyeUV.x = minX; + currentLeftEyeUV.y = minY; + currentLeftEyeUV.w = maxX - minX; + currentLeftEyeUV.h = maxY - minY; + ClampCurrentUV(); + SyncRightEyeUV(); + + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + isDraggingCrop = false; + } + } + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 pMin(cursorPos.x + currentLeftEyeUV.x * imageSize.x, cursorPos.y + currentLeftEyeUV.y * imageSize.y); + ImVec2 pMax(cursorPos.x + (currentLeftEyeUV.x + currentLeftEyeUV.w) * imageSize.x, + cursorPos.y + (currentLeftEyeUV.y + currentLeftEyeUV.h) * imageSize.y); + drawList->AddRect(pMin, pMax, IM_COL32(0, 255, 0, 255), 0.0f, 0, 2.0f); + } + + PixelRegion Controller::GetLeftEyePixelRegion(uint32_t fullTextureWidth, uint32_t fullTextureHeight) const + { + return UVToPixelRegion(currentLeftEyeUV, fullTextureWidth / 2, fullTextureHeight); + } + + StereoPixelRegions Controller::GetStereoPixelRegions(uint32_t fullTextureWidth, uint32_t fullTextureHeight) const + { + StereoPixelRegions regions; + regions.leftEye = UVToPixelRegion(currentLeftEyeUV, fullTextureWidth / 2, fullTextureHeight); + regions.rightEye = UVToPixelRegion(currentRightEyeUV, fullTextureWidth / 2, fullTextureHeight); + return regions; + } + + void Controller::EnsureDefaultPreset() + { + if (presets.empty()) { + Preset preset; + preset.name = "Full Left Eye"; + preset.leftEye = DefaultUV(); + preset.rightEye = DefaultUV(); + presets.push_back(std::move(preset)); + } + } + + void Controller::SyncRightEyeUV() + { + // Mirror horizontally: left-eye overlap is toward the nose (right), + // right-eye overlap is toward the nose (left). + currentRightEyeUV.y = currentLeftEyeUV.y; + currentRightEyeUV.w = currentLeftEyeUV.w; + currentRightEyeUV.h = currentLeftEyeUV.h; + currentRightEyeUV.x = 1.0f - currentLeftEyeUV.x - currentLeftEyeUV.w; + } + + void Controller::ClampCurrentUV() + { + currentLeftEyeUV = ClampUV(currentLeftEyeUV); + currentRightEyeUV = ClampUV(currentRightEyeUV); + } + + void Controller::ApplyPreset(int index) + { + EnsureDefaultPreset(); + selectedPresetIndex = std::clamp(index, 0, static_cast(presets.size()) - 1); + currentLeftEyeUV = presets[selectedPresetIndex].leftEye; + currentRightEyeUV = presets[selectedPresetIndex].rightEye; + ClampCurrentUV(); + } +} \ No newline at end of file diff --git a/src/Utils/Subrect/Subrect.h b/src/Utils/Subrect/Subrect.h new file mode 100644 index 0000000000..b9355742b9 --- /dev/null +++ b/src/Utils/Subrect/Subrect.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +namespace Subrect +{ + struct UVRegion + { + float x = 0.0f; + float y = 0.0f; + float w = 1.0f; + float h = 1.0f; + }; + + struct PixelRegion + { + uint32_t x = 0; + uint32_t y = 0; + uint32_t w = 1; + uint32_t h = 1; + }; + + struct StereoPixelRegions + { + PixelRegion leftEye; + PixelRegion rightEye; + }; + + struct Preset + { + std::string name; + UVRegion leftEye; + UVRegion rightEye; + }; + + class Controller + { + public: + void LoadSettings(const json& a_json); + void SaveSettings(json& a_json) const; + void DrawEditor(ID3D11ShaderResourceView* previewSrv, ID3D11Texture2D* previewTexture, float eyeRatio); + + PixelRegion GetLeftEyePixelRegion(uint32_t fullTextureWidth, uint32_t fullTextureHeight) const; + StereoPixelRegions GetStereoPixelRegions(uint32_t fullTextureWidth, uint32_t fullTextureHeight) const; + + const UVRegion& GetLeftEyeUV() const { return currentLeftEyeUV; } + const UVRegion& GetRightEyeUV() const { return currentRightEyeUV; } + + private: + std::vector presets; + int selectedPresetIndex = 0; + char newPresetName[64] = ""; + + UVRegion currentLeftEyeUV{}; + UVRegion currentRightEyeUV{}; + + bool isDraggingCrop = false; + float dragStartUV[2] = { 0.0f, 0.0f }; + + void EnsureDefaultPreset(); + void SyncRightEyeUV(); + void ClampCurrentUV(); + void ApplyPreset(int index); + }; +} \ No newline at end of file