diff --git a/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl b/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl new file mode 100644 index 0000000000..266fe5fd50 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl @@ -0,0 +1,97 @@ +// Stretches the DRS-rendered region from a temporary render-resolution SBS texture +// to fill the entire eye in the display-resolution kMAIN SBS texture. +// Dispatched once per eye. Supports multiple sampling modes: +// 0 = Bilinear (clean upscale) +// 1 = Point / Nearest (cheapest, VRS-like broadcast) +// 2 = Gaussian Blur 3x3 (soft periphery background) + +cbuffer StretchCB : register(b0) +{ + uint DstOffsetX; // SBS destination X offset for this eye (0 or eyeWidthOut) + uint DstWidth; // display-resolution eye width + uint DstHeight; // display-resolution eye height + uint SrcOffsetX; // SBS source X offset for this eye (0 or renderEyeW) + uint SrcWidth; // render-resolution SBS total width (for UV normalisation) + uint SrcHeight; // render-resolution SBS total height + uint SrcEyeWidth; // render-resolution per-eye width + uint SrcEyeHeight; // render-resolution per-eye height + uint StretchMode; // 0=Bilinear, 1=Point, 2=GaussianBlur + float BlurRadius; // Texel-space radius for Gaussian blur (typical 0.5-4.0) + uint DebugVisualize; // 0=off, 1=tint stretched periphery red so the DLSS region pops + uint _pad; +}; + +Texture2D SrcTex : register(t0); +SamplerState BilinearSampler : register(s0); +RWTexture2D DstTex : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 tid : SV_DispatchThreadID) { + // Zero-dim guard: a misconfigured dispatch with any zero extent would + // divide-by-zero into NaN UVs and underflow point-mode coords into + // huge uint values. Bail before any math. + if (DstWidth == 0 || DstHeight == 0 || SrcWidth == 0 || SrcHeight == 0 || + SrcEyeWidth == 0 || SrcEyeHeight == 0) + return; + + if (tid.x >= DstWidth || tid.y >= DstHeight) + return; + + // Map output pixel to normalised position within this eye [0,1] + float u = ((float)tid.x + 0.5) / (float)DstWidth; + float v = ((float)tid.y + 0.5) / (float)DstHeight; + + // Map to source texel coordinates within this eye's render region + // then convert to full SBS texture UV (adding eye offset) + float srcU = (u * (float)SrcEyeWidth + (float)SrcOffsetX) / (float)SrcWidth; + float srcV = (v * (float)SrcEyeHeight) / (float)SrcHeight; + + // Clamp sample UVs to per-eye texel bounds so the bilinear footprint and + // blur kernel can't reach across the SBS midline into the neighboring + // eye's pixels. + float2 eyeMinUV = float2(((float)SrcOffsetX + 0.5) / (float)SrcWidth, + 0.5 / (float)SrcHeight); + float2 eyeMaxUV = float2(((float)(SrcOffsetX + SrcEyeWidth) - 0.5) / (float)SrcWidth, + ((float)SrcEyeHeight - 0.5) / (float)SrcHeight); + + float4 color; + + if (StretchMode == 1) { + // Point / Nearest: integer texel lookup, cheapest. min() keeps us + // inside [0, SrcEyeWidth-1] / [0, SrcEyeHeight-1] when u/v == 1. + uint2 srcPixel = uint2( + min((uint)(u * (float)SrcEyeWidth), SrcEyeWidth - 1) + SrcOffsetX, + min((uint)(v * (float)SrcEyeHeight), SrcEyeHeight - 1)); + color = SrcTex.Load(int3(srcPixel, 0)); + } else if (StretchMode == 2) { + // Gaussian blur 3x3: 9-tap weighted average around center + float2 texelSize = float2(1.0 / (float)SrcWidth, 1.0 / (float)SrcHeight); + float2 center = float2(srcU, srcV); + float2 step = texelSize * BlurRadius; + + // Gaussian weights for 3x3 kernel (sigma ~ 0.85 * radius) + // Center=4, Edge=2, Corner=1, sum=16 + float4 sum = SrcTex.SampleLevel(BilinearSampler, clamp(center, eyeMinUV, eyeMaxUV), 0) * 4.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, 0), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, 0), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(0, -step.y), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(0, step.y), eyeMinUV, eyeMaxUV), 0) * 2.0; + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, -step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, -step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(-step.x, step.y), eyeMinUV, eyeMaxUV), 0); + sum += SrcTex.SampleLevel(BilinearSampler, clamp(center + float2(step.x, step.y), eyeMinUV, eyeMaxUV), 0); + color = sum * (1.0 / 16.0); + } else { + // Bilinear (default): single hardware-filtered sample + color = SrcTex.SampleLevel(BilinearSampler, clamp(float2(srcU, srcV), eyeMinUV, eyeMaxUV), 0); + } + + // Debug visualizer: tint the cheap-stretched periphery red so the DLSS + // subrect (which BlendSubrectToOutput overwrites on top of us) reads as + // the un-tinted region. Lets users see at a glance where DLSS is actually + // reconstructing vs where the cheap stretch is filling. + if (DebugVisualize != 0) { + color.rgb = lerp(color.rgb, color.rgb * float3(1.6, 0.35, 0.35), 0.6); + } + + DstTex[uint2(tid.x + DstOffsetX, tid.y)] = color; +} diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index 8f6d445293..68ea6502c3 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -8,6 +8,7 @@ #include "Globals.h" #include "Menu.h" #include "Utils/FileSystem.h" +#include "Utils/Subrect.h" #include #include #include @@ -273,35 +274,6 @@ namespace combo[0].GetKey() == VK_SNAPSHOT; } - // Blend state used around the preview's ImGui::Image draw. Two regression - // risks if this is changed: - // 1. BlendEnable must stay FALSE - the source texture carries non-1 alpha - // where Skyrim composited UI plates; default SRC_ALPHA blend lets the - // host window background show through (visible on the desktop mirror). - // 2. WriteMask must exclude alpha (RGB only). In VR, Skyrim's menu UI - // shader recomposites our menu plate over the SBS framebuffer with - // alpha blending; writing texture alpha into the menu plate RT - // produces a cutout visible only through the HMD. RGB-only writes - // leave the plate's pre-cleared alpha=1 in place. - // Paired with ImDrawCallback_ResetRenderState queued by Subrect::DrawEditor - // immediately after the image draw. - void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) - { - static winrt::com_ptr opaqueBlend; - if (!opaqueBlend) { - D3D11_BLEND_DESC desc{}; - desc.RenderTarget[0].BlendEnable = FALSE; - desc.RenderTarget[0].RenderTargetWriteMask = - D3D11_COLOR_WRITE_ENABLE_RED | - D3D11_COLOR_WRITE_ENABLE_GREEN | - D3D11_COLOR_WRITE_ENABLE_BLUE; - globals::d3d::device->CreateBlendState(&desc, opaqueBlend.put()); - } - if (opaqueBlend) { - globals::d3d::context->OMSetBlendState(opaqueBlend.get(), nullptr, 0xFFFFFFFF); - } - } - std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath) { SYSTEMTIME st; @@ -429,7 +401,7 @@ void ScreenshotFeature::DrawSettings() } } - subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, OpaquePreviewBlendCallback); + subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, Util::Subrect::OpaquePreviewBlendCallback); } void ScreenshotFeature::EnsurePreviewCache(ID3D11Texture2D* sourceTexture) diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 32c0cd170c..c793eabc1b 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -4,9 +4,14 @@ #include "HDRDisplay.h" #include "Hooks.h" #include "State.h" -#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" +#include "Upscaling/FoveatedRender.h" +#include "Upscaling/FoveatedRender/Bridge.h" +#include "Upscaling/FoveatedRender/Core.h" +#include "Upscaling/FoveatedRender/Postprocess.h" +#include "Upscaling/FoveatedRender/Preprocess.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/Streamline.h" #include "Utils/UI.h" #include @@ -272,7 +277,7 @@ void Upscaling::DrawSettings() // Derive scale from live `settings.qualityMode` — `resolution- // Scale` is locked to the PerfMode boot snapshot, so reusing it // here would mismatch the slider position the user sees. - const float displayScale = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)std::clamp(settings.qualityMode, 0u, 4u)); + const float displayScale = 1.0f / GetQualityModeRatio(settings.qualityMode); std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, displayScale); ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); @@ -286,7 +291,7 @@ void Upscaling::DrawSettings() const char* bootLabel = (upscaleMethod == UpscaleMethod::kDLSS) ? upscalePresetsDLSS[std::clamp(4 - (int)bm, 0, 4)] : upscalePresets[std::clamp(4 - (int)bm, 0, 4)]; Util::Text::RestartNeeded( "Pending restart: currently active = %s ( %.2fx ). Change applies after game restart.", - bootLabel, 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)bm)); + bootLabel, 1.0f / GetQualityModeRatio(bm)); } } @@ -472,6 +477,25 @@ void Upscaling::DrawSettings() ImGui::TreePop(); } + // FoveatedRender: foveated subrect DLSS — VR-only, opt-in mode of this + // feature. Like DLSSperf, lives here rather than as a peer Feature so + // all DLSS surfaces share one settings panel. Enable lives at the top + // level for discoverability; the body knobs are collapsed by default and + // greyed out until the user opts in. + if (globals::game::isVR) { + ImGui::Separator(); + foveatedRender.DrawEnable(); + const bool enabled = foveatedRender.settings.enabled != 0; + if (!enabled) + ImGui::BeginDisabled(); + if (ImGui::TreeNodeEx("Foveated DLSS — Tuning")) { + foveatedRender.DrawSettings(); + ImGui::TreePop(); + } + if (!enabled) + ImGui::EndDisabled(); + } + if (ImGui::TreeNodeEx("Backend Diagnostics")) { // Streamline log level selection const char* logLevels[] = { "Off", "Default", "Verbose" }; @@ -556,6 +580,11 @@ void Upscaling::DrawSettings() void Upscaling::SaveSettings(json& o_json) { o_json = settings; + // Nest FoveatedRender's settings under a sub-key so they round-trip alongside + // Upscaling's own. Subrect controller persistence is owned by FoveatedRender. + json foveatedRenderJson; + foveatedRender.SaveSettings(foveatedRenderJson); + o_json["foveatedRender"] = foveatedRenderJson; auto iniSettingCollection = globals::game::iniPrefSettingCollection; if (iniSettingCollection) { auto setting = iniSettingCollection->GetSetting("bUseTAA:Display"); @@ -567,6 +596,15 @@ void Upscaling::SaveSettings(json& o_json) void Upscaling::LoadSettings(json& o_json) { + // Pull FoveatedRender's nested block first so its absence doesn't fail the + // outer settings deserialize. FoveatedRender::ClampSettings touches sibling + // presetDLSS (cross-feature compat), so re-run it after `settings = o_json` + // below — otherwise the JSON re-assign overwrites the clamp and an + // incompatible preset slips through. (Copilot on PR #44.) + if (o_json.contains("foveatedRender")) { + foveatedRender.LoadSettings(o_json["foveatedRender"]); + o_json.erase("foveatedRender"); + } settings = o_json; // Sanitize loaded settings to ensure enum indices are valid @@ -587,6 +625,12 @@ void Upscaling::LoadSettings(json& o_json) logger::warn("[Upscaling] Loaded presetDLSS {} out of range, resetting to 0 (Default)", settings.presetDLSS); settings.presetDLSS = 0; } + // Re-apply FoveatedRender's cross-feature clamp now that the JSON + // re-assign above has overwritten anything it set during its own + // LoadSettings (which fired before this block ran). Idempotent — no-op + // if FoveatedRender is inactive or the preset is already compatible. + // (Copilot on PR #44.) + foveatedRender.ClampSettings(); const float originalReflexFPSLimit = settings.reflexFPSLimit; if (!std::isfinite(settings.reflexFPSLimit)) { settings.reflexFPSLimit = 60.0f; @@ -615,6 +659,7 @@ void Upscaling::LoadSettings(json& o_json) void Upscaling::RestoreDefaultSettings() { settings = {}; + foveatedRender.RestoreDefaultSettings(); } void Upscaling::DataLoaded() @@ -672,6 +717,10 @@ struct BSImageSpace_Init_FXAA }; void Upscaling::PostPostLoad() { + // Subrect controller defaults + stereo flag (FoveatedRender is no longer a + // Feature subclass so we drive its lifecycle from here). + foveatedRender.PostPostLoad(); + bool isGOG = !GetModuleHandle(L"steam_api64.dll"); stl::detour_thunk(REL::RelocationID(79947, 82084)); @@ -701,6 +750,19 @@ void Upscaling::PostPostLoad() logger::info("[Upscaling] Installed hooks"); } +float Upscaling::GetQualityModeRatio(uint qualityMode) +{ + // Lower bound is 0, not 1: qualityMode=0 is DLAA / NATIVEAA (1.0x — + // render at display resolution). The FfxFsr3QualityMode enum header + // doesn't *declare* a 0 value, but the implementation delegates to + // FfxFsr3UpscalerQualityMode which has NATIVEAA=0 → 1.0f. Clamping to + // 1 would force DLAA into Quality (1.5x) and shrink the rendered + // region of kMAIN to 67%. + const float ratio = ffxFsr3GetUpscaleRatioFromQualityMode( + static_cast(std::clamp(qualityMode, 0u, 4u))); + return std::isfinite(ratio) && ratio > 0.0f ? ratio : 3.0f; +} + Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const { // Lock runtime to the boot upscaler under PerfMode — engine RTs are @@ -1342,7 +1404,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) // Boot qualityMode under PerfMode so projection stays coherent // with the engine RTs sized at install. const uint32_t qm = globals::features::upscaling.perfMode.IsHookActive() ? bootSnapshot.Boot(&Settings::qualityMode) : settings.qualityMode; - float resolutionScaleBase = 1.0f / ffxFsr3GetUpscaleRatioFromQualityMode((FfxFsr3QualityMode)qm); + float resolutionScaleBase = 1.0f / GetQualityModeRatio(qm); auto renderWidth = static_cast(screenWidth * resolutionScaleBase); auto renderHeight = static_cast(screenHeight * resolutionScaleBase); @@ -1480,6 +1542,7 @@ void Upscaling::SetupResources() void Upscaling::ClearShaderCache() { + foveatedRender.ClearShaderCache(); for (int i = 0; i < 5; ++i) { encodeTexturesCS[i] = nullptr; // com_ptr automatically releases } @@ -1909,7 +1972,42 @@ void Upscaling::Upscale() logger::debug("[Upscaling] LoadingMenu close detected — rebuilding DLSS feature"); streamline.DestroyDLSSResources(); } - streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); + + // PR-3 MVP-B: opt-in FoveatedRender route. When active, runs the + // per-eye DLSS dispatch with optional foveal subrect through + // FoveatedRenderImpl::Core; falls through to dev's standard path on + // any failure so users always see DLSS output (graceful + // degradation — no black frames if the enhancer preflights bad). + // + // Menu-skip: in menus the world stops producing fresh motion + // vectors and depth, but kMAIN keeps changing (UI plate composites). + // The route's subrect DLSS evaluate then accumulates temporal + // history against stale neighbourhood data and the subrect region + // renders as visible reconstruction garbage. Standard full-eye DLSS + // (the fall-through below) is robust to this because it reconstructs + // across the whole image — the foveated crop is what makes the + // stale-history bleed visible. Same menu-open predicate dev uses + // at Upscaling.cpp:1748 for ShouldUseFrameGenerationThisFrame. + auto* ui = globals::game::ui; + auto* st = globals::state; + const bool menuOpen = (ui && ui->GameIsPaused()) || (st && st->IsMainOrLoadingMenuOpen(ui)); + bool routeHandled = false; + if (FoveatedRenderImpl::Bridge::IsRouteActive() && globals::game::isVR && !menuOpen) { + if (FoveatedRenderImpl::Preprocess::EncodeUpscalingTextures(*this)) { + routeHandled = FoveatedRenderImpl::Core::ExecuteVRDlssCore(streamline, + main.texture, + globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].texture, + reactiveMaskTexture->resource.get(), + transparencyCompositionMaskTexture->resource.get(), + motionVectorCopyTexture->resource.get()); + if (!routeHandled) { + logger::warn("[FOVEATED] route preflight failed — falling through to standard DLSS path"); + } + } + } + if (!routeHandled) { + streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); + } } else if (upscaleMethod == UpscaleMethod::kFSR) { // PerfMode bridge: when the engine RTs are shrunk to renderRes, FSR's displayRes // output must land in perfMode.testTexture (the private displayRes target used for @@ -2313,8 +2411,19 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 upscaling.UpscaleDepth(); } - if (upscaleMethod == UpscaleMethod::kDLSS) - upscaling.ApplySharpening(); + if (upscaleMethod == UpscaleMethod::kDLSS) { + // FoveatedRender's DLSS output doesn't land in sharpenerTexture the + // way dev's path does (the route writes to its own per-eye intermediates + // and copies back to kMAIN/testTexture), so dev's zero-copy + // ApplySharpening can't read sharpenerTexture. Route through + // Postprocess::ApplyDlssSharpening which does the kMAIN → sharpener → + // kMAIN round-trip. Both paths honor sharpnessDLSS=0 to disable RCAS. + if (FoveatedRenderImpl::Bridge::IsRouteActive()) { + FoveatedRenderImpl::Postprocess::ApplyDlssSharpening(upscaling); + } else { + upscaling.ApplySharpening(); + } + } auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 9fbdcf47a4..d84b422854 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -1,9 +1,10 @@ #pragma once #include "Feature.h" -#include "Upscaling/PerfMode.h" #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" +#include "Upscaling/FoveatedRender.h" +#include "Upscaling/PerfMode.h" #include "Upscaling/RCAS/RCAS.h" #include "Upscaling/Streamline.h" #include "Utils/BootSnapshot.h" @@ -160,6 +161,16 @@ struct Upscaling : Feature UpscaleMethod GetUpscaleMethod() const; + /// Render-to-display scale ratio for a quality mode index + /// (1=Quality, 2=Balanced, 3=Performance, 4=UltraPerformance). + /// Single source of truth across DLSS, FSR, and FoveatedRender paths: + /// the four "quality preset" ratios (1.5/1.7/2.0/3.0) are aligned across + /// DLSS and FSR3 by NVIDIA's DLSS Programming Guide and FFX's + /// FfxFsr3QualityMode enum, so all upscalers in this plugin route their + /// quality lookups through here rather than duplicating the table. Returns + /// 3.0 (UltraPerformance) on out-of-range input. + static float GetQualityModeRatio(uint qualityMode); + void CheckResources(UpscaleMethod a_upscalemethod); void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod); void DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod); @@ -234,8 +245,9 @@ struct Upscaling : Feature static inline Streamline streamline; static inline FidelityFX fidelityFX; ///< Only for frame generation static inline DX12SwapChain dx12SwapChain; - static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS - static inline PerfMode perfMode; ///< VR-only: render engine at upscaled-render res + static inline RCAS rcas; ///< Standalone RCAS sharpening for DLSS + static inline PerfMode perfMode; ///< VR-only: render engine at upscaled-render res + static inline FoveatedRender foveatedRender; ///< VR-only: foveated subrect DLSS winrt::com_ptr copyDepthToSharedBufferPS; diff --git a/src/Features/Upscaling/FoveatedRender.cpp b/src/Features/Upscaling/FoveatedRender.cpp new file mode 100644 index 0000000000..52cc6cfbfc --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender.cpp @@ -0,0 +1,323 @@ +#include "FoveatedRender.h" + +#include "../../Globals.h" +#include "../../Utils/Subrect.h" +#include "../../Utils/UI.h" +#include "../Upscaling.h" +#include "FoveatedRender/Core.h" + +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + FoveatedRender::Settings, + enabled, + dlssMode, + stretchMode, + debugVisualize); + +// ============================================================================ +// Lifecycle +// ============================================================================ + +void FoveatedRender::PostPostLoad() +{ + // Opt into PR-1's stereo extension so the controller tracks a separate + // right-eye UV (HMD nose-side overlap symmetry). + subrectController.SetStereoEnabled(true); + + // Seed sensible foveal presets. Empty-case only — user edits persist. + // "Center N%" presets are symmetric per eye (no rightUV → auto-mirror, which + // for centered UVs produces an identical right-eye UV). "Nasal Convergence" + // is asymmetric: left eye biased toward its right edge, right eye biased + // toward its left edge — both targeting the nose-side region where HMD + // binocular fusion is strongest, so DLSS reconstruction lands in the actual + // stereo overlap zone rather than diverging left/right fields. + subrectController.SeedDefaultPresets({ + { .name = "Full Eye", .uv = { 0.0f, 0.0f, 1.0f, 1.0f } }, + { .name = "Center 75%", .uv = { 0.125f, 0.125f, 0.75f, 0.75f } }, + { .name = "Center 50%", .uv = { 0.25f, 0.25f, 0.5f, 0.5f } }, + { .name = "Nasal Convergence 50%", + .uv = { 0.5f, 0.25f, 0.5f, 0.5f }, + .rightUV = Util::Subrect::UVRegion{ 0.0f, 0.25f, 0.5f, 0.5f } }, + }); +} + +void FoveatedRender::ClearShaderCache() +{ + FoveatedRenderImpl::Core::ClearShaderCache(); +} + +// ============================================================================ +// Settings I/O — driven from Upscaling::Save/LoadSettings under a nested key +// ============================================================================ + +void FoveatedRender::SaveSettings(json& o_json) +{ + o_json = settings; + subrectController.SaveSettings(o_json); +} + +void FoveatedRender::LoadSettings(const json& o_json) +{ + settings = o_json; + // Util::Subrect::Controller::LoadSettings takes `const json&` (Subrect.h:68) + // so no const_cast is needed — keeping it would imply mutation that never + // happens. (Copilot + CodeRabbit on PR #44.) + subrectController.LoadSettings(o_json); + ClampSettings(); +} + +void FoveatedRender::RestoreDefaultSettings() +{ + settings = {}; + ClampSettings(); +} + +void FoveatedRender::ClampSettings() +{ + settings.enabled = std::min(settings.enabled, 1u); + settings.dlssMode = std::min(settings.dlssMode, 1u); + settings.stretchMode = std::min(settings.stretchMode, 2u); + settings.debugVisualize = std::min(settings.debugVisualize, 1u); + // Preset clamping reads from Upscaling::Settings now. + auto& sharedPreset = globals::features::upscaling.settings.presetDLSS; + sharedPreset = std::min(sharedPreset, 5u); + if (!IsPresetCompatibleWithMode(sharedPreset)) { + sharedPreset = 3; // Fall back to L + } +} + +// ============================================================================ +// Activation + accessors +// ============================================================================ + +bool FoveatedRender::IsActive() const +{ + return enabledAtBoot && IsRuntimeSupported(); +} + +bool FoveatedRender::IsRuntimeSupported() const +{ + return globals::game::isVR && globals::features::upscaling.streamline.featureDLSS; +} + +void FoveatedRender::LatchQualityMode() +{ + qualityModeAtBoot = std::clamp(globals::features::upscaling.settings.qualityMode, 1u, 4u); +} + +uint FoveatedRender::GetActiveQualityMode() const +{ + return std::clamp(globals::features::upscaling.settings.qualityMode, 1u, 4u); +} + +uint FoveatedRender::GetActivePresetDLSS() const +{ + return std::min(globals::features::upscaling.settings.presetDLSS, 5u); +} + +float FoveatedRender::GetActiveSharpnessDLSS() const +{ + return std::clamp(globals::features::upscaling.settings.sharpnessDLSS, 0.0f, 1.0f); +} + +float FoveatedRender::GetRenderScaleForQuality(uint qualityMode) +{ + return Upscaling::GetQualityModeRatio(qualityMode); +} + +bool FoveatedRender::IsPresetCompatibleWithMode(uint presetIndex) const +{ + // Preset indices: 0=Default, 1=J, 2=K, 3=L, 4=M, 5=F + // Faster mode: J(1) and K(2) are incompatible. + if (GetDlssMode() == DlssMode::kFaster) { + return presetIndex != 1 && presetIndex != 2; + } + return true; +} + +void FoveatedRender::ClampPresetToMode() +{ + auto& sharedPreset = globals::features::upscaling.settings.presetDLSS; + if (!IsPresetCompatibleWithMode(sharedPreset)) { + sharedPreset = 3; // Fall back to L + } +} + +// ============================================================================ +// UI — FoveatedRender-specific knobs only. Quality / sharpness / preset / +// Streamline log level live on Upscaling's panel and apply to both DLSS paths. +// Called from Upscaling::DrawSettings inside a TreeNode. +// ============================================================================ + +void FoveatedRender::DrawEnable() +{ + ClampSettings(); + + ImGui::TextWrapped( + "Foveated subrect DLSS: only the user-selected region gets full DLSS upscaling, " + "the periphery is cheaply stretched. Significant DLSS cost reduction at the cost " + "of peripheral sharpness. VR + DLSS only."); + + const bool runtimeSupported = IsRuntimeSupported(); + if (!runtimeSupported) { + settings.enabled = 0; + } + + if (!runtimeSupported) + ImGui::BeginDisabled(); + bool enabledBool = settings.enabled != 0; + if (ImGui::Checkbox("Enable Foveated DLSS", &enabledBool)) + settings.enabled = enabledBool ? 1u : 0u; + if (!runtimeSupported) + ImGui::EndDisabled(); + + if ((settings.enabled != 0) != enabledAtBoot) { + Util::Text::RestartNeeded("Pending restart: FoveatedRender will %s on next launch.", + settings.enabled ? "enable" : "disable"); + } + + if (enabledAtBoot) { + Util::Text::WrappedInfo("Active: upscaling is forced to DLSS while enabled."); + } + + if (!globals::game::isVR) { + Util::Text::Warning("VR only. Non-VR / FSR support pending future contributors."); + } + if (globals::game::isVR && !globals::features::upscaling.streamline.featureDLSS) { + Util::Text::Warning("DLSS runtime not available. Enable is blocked."); + } +} + +void FoveatedRender::DrawSettings() +{ + static const char* dlssModes[] = { "Default", "Faster" }; + static const char* stretchModes[] = { "Bilinear", "Point", "Gaussian Blur" }; + + ClampSettings(); + + Util::Text::WrappedInfo("Quality / Sharpness / DLSS Preset / Streamline log level are shared with the standard DLSS path above."); + + // ── VR-only knobs ── + if (globals::game::isVR) { + ImGui::Separator(); + ImGui::Text("VR DLSS Mode"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Default vs Faster: trade per-eye image quality for setup cost. Switch only when you\n" + "can see a difference in your scene — otherwise prefer Faster.\n" + "\n" + "Default — use when: image quality matters more than the small overhead — cinematic\n" + "scenes, screenshot/recording, or if you notice ghosting/edge artifacts in Faster.\n" + "Each eye gets isolated per-eye intermediates for color/depth/MV/reactive/transparency\n" + "so DLSS can't sample across the SBS midline. Costs five per-eye copies per frame.\n" + "All DLSS presets (Default, J, K, L, M, F) supported.\n" + "\n" + "Faster — use when: you want the cheapest foveated path and aren't seeing artifacts —\n" + "fast-motion gameplay, exploration, anywhere small quality losses go unnoticed.\n" + "DLSS reads kMAIN directly via extent offsets, so bilinear sampling can touch 1-2\n" + "texels of the neighboring eye near the SBS midline. We snapshot kMAIN once and\n" + "clear the HMD hidden-area ring to prevent sky-blue bleed on fast head motion.\n" + "Presets J and K are unavailable — switching here auto-clamps preset to L."); + } + + uint prevMode = settings.dlssMode; + ImGui::SliderInt("DLSS Mode", reinterpret_cast(&settings.dlssMode), 0, 1, dlssModes[settings.dlssMode]); + if (settings.dlssMode != prevMode) { + const uint prevPreset = globals::features::upscaling.settings.presetDLSS; + ClampPresetToMode(); + if (globals::features::upscaling.settings.presetDLSS != prevPreset) { + logger::info("[FOVEATED] DLSS preset clamped from {} to {} after Faster switch (J/K incompatible)", + prevPreset, globals::features::upscaling.settings.presetDLSS); + } + } + switch (GetDlssMode()) { + case DlssMode::kDefault: + ImGui::TextWrapped("Per-eye isolation: 2 resource sets, 2 DLSS evaluates."); + break; + case DlssMode::kFaster: + ImGui::TextWrapped("SBS viewport: 1 snapshot + 2 mask clears, 2 evaluates. Presets J/K unavailable."); + break; + } + + ImGui::Separator(); + ImGui::Text("Background Stretch"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "How the cheap periphery is reconstructed to fill the area outside the DLSS subrect.\n" + "This is the cost-saving step — DLSS only runs on the subrect, the rest is filled by\n" + "this cheaper pass. Only affects pixels outside your selected region."); + } + ImGui::SliderInt("Stretch Mode", reinterpret_cast(&settings.stretchMode), 0, 2, stretchModes[settings.stretchMode]); + switch (GetStretchMode()) { + case StretchMode::kBilinear: + ImGui::TextWrapped("Bilinear: clean linear upscale. Looks like a soft DLSS-Performance result."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the periphery to look like a sensible low-quality reconstruction,\n" + "close to how DLSS-Performance would look. Default-ish choice.\n" + "\n" + "Visual artifact: typical bilinear softness — fine geometry in the periphery looks\n" + "slightly out of focus but not visibly stretched."); + } + break; + case StretchMode::kPoint: + ImGui::TextWrapped("Point (nearest-neighbor): cheapest. Visibly pixelated periphery."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the smallest possible cost in the periphery and don't mind\n" + "obvious pixelation outside your gaze region. Useful for benchmarking the upper\n" + "bound of foveated savings.\n" + "\n" + "Visual artifact: chunky pixel blocks in the periphery, very visible if you look\n" + "away from the subrect center."); + } + break; + case StretchMode::kGaussianBlur: + ImGui::TextWrapped("Gaussian blur: softens periphery further. Hides upscale artifacts behind blur."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Use when: you want the periphery to fall away into soft focus — closer to how\n" + "natural human peripheral vision feels. Good default for actual foveated use.\n" + "\n" + "Visual artifact: noticeable blur in the periphery. If your subrect is large this\n" + "is barely visible; if small, the blur is the dominant visual signal."); + } + break; + } + + ImGui::Separator(); + ImGui::Text("Subrect Region"); + ImGui::TextWrapped( + "Drag in the preview below to select the region that gets full DLSS upscaling. " + "The rest is cheaply stretched — saves significant DLSS cost."); + Util::Text::WrappedInfo("Screenshot has its own subrect; align them only if you want pixel-matched captures."); + + bool debugBool = settings.debugVisualize != 0; + if (ImGui::Checkbox("Visualize regions", &debugBool)) + settings.debugVisualize = debugBool ? 1u : 0u; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Diagnostic: tint the cheap-stretched periphery red so the DLSS-reconstructed\n" + "subrect (un-tinted) pops visually in-game. Lets you confirm at a glance where\n" + "DLSS is actually running vs where the cheap stretch is filling. No perf impact;\n" + "runtime toggle, no restart needed."); + } + + // Preview off kVR_FRAMEBUFFER (the final composed SBS image the headset + // sees) rather than kMAIN. kMAIN is mid-pipeline and carries non-1 + // alpha where Skyrim composited UI plates, so even with the opaque + // blend callback you see the menu mask outline instead of the rendered + // world. ScreenshotFeature picks the same RT for the same reason + // (ScreenshotFeature.cpp:243). Foveated is VR-only so kVR_FRAMEBUFFER + // is always populated when we get here. + auto renderer = globals::game::renderer; + if (renderer) { + auto& fb = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kVR_FRAMEBUFFER]; + auto* tex = static_cast(fb.texture); + subrectController.DrawEditor(fb.SRV, tex, 0.5f, 0.0f, Util::Subrect::OpaquePreviewBlendCallback); + } else { + subrectController.DrawEditor(nullptr, nullptr, 0.5f); + } + } +} diff --git a/src/Features/Upscaling/FoveatedRender.h b/src/Features/Upscaling/FoveatedRender.h new file mode 100644 index 0000000000..0d7ee3b62c --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender.h @@ -0,0 +1,111 @@ +#pragma once + +// ============================================================================ +// FoveatedRender — VR DLSS enhancement mode of Upscaling +// ============================================================================ +// +// Foveated subrect-DLSS path: only the user-selected region gets full DLSS +// upscaling; the periphery is cheaply stretched via SubrectStretchCS. Halves +// (or more) the DLSS workload. Composes with VRS, Screenshot, and the lossless +// recording feature through the shared Util::Subrect module — use the same +// preset for consistent results across them. +// +// Architecturally a mode inside Upscaling (mirroring DLSSperf): a static- +// inline member, not a peer Feature. Settings that overlap with Upscaling's +// (quality mode, sharpness, DLSS preset, Streamline log level) read directly +// from `globals::features::upscaling.settings` rather than being duplicated. +// VR + DLSS only at present; non-VR / FSR extension is left to future work. +// +// ============================================================================ + +#include "../../Utils/Subrect.h" + +struct FoveatedRender +{ + // DLSS execution mode for VR + enum class DlssMode : uint + { + kDefault = 0, // Per-eye isolation: 2 extra resource sets, 2 evaluates. Supports F/J/K/L/M. + kFaster = 1, // SBS viewport: tell SL to read subrect from SBS directly, no extra resources, 2 evaluates. J/K incompatible, only L/M/F. + }; + + // Stretch algorithm for DRS → full-eye background (used by SubrectStretchCS shader) + enum class StretchMode : uint + { + kBilinear = 0, // Default bilinear sampling (clean upscale) + kPoint = 1, // Nearest-neighbor / point (cheapest, VRS-like broadcast) + kGaussianBlur = 2, // 3x3 Gaussian blur (soft periphery) + }; + + // FoveatedRender-specific settings. Quality mode / sharpness / DLSS preset / + // Streamline log level live on Upscaling::Settings and are read through + // the accessors below — do not duplicate them here. Sharpening on/off is + // controlled by the shared sharpnessDLSS slider (0 disables RCAS). + // + // Deferred to PR-3b: per-input DLSS hint toggles (MV dilation, reactive mask, + // transparency mask). The original PR #2096 declared the Settings fields and + // UI sliders but never plumbed them to EncodeTexturesCS or to the EvaluateDLSS + // arg list, so they were no-ops there too. Bringing them back in PR-3b means + // shader permutations (per-toggle defines), conditional encode-pass skip when + // all are off, and per-toggle DLSS arg gating — ship the implementation and + // the UI together so the knobs don't lie. + struct Settings + { + uint enabled = 0; // opt-in: requires restart to take effect via LatchEnabled() + uint dlssMode = (uint)DlssMode::kDefault; + uint stretchMode = (uint)StretchMode::kGaussianBlur; + uint debugVisualize = 0; // tint cheap-stretched periphery red; runtime toggle + }; + + Settings settings; + Util::Subrect::Controller subrectController; + + // Called from Upscaling::DrawSettings. DrawEnable renders the always-visible + // header + Enable checkbox at the parent's top level; DrawSettings renders + // the body knobs inside a collapsible TreeNode (Upscaling wraps it in + // BeginDisabled when settings.enabled == 0). + void DrawEnable(); + void DrawSettings(); + // Called from Upscaling::SaveSettings / LoadSettings to round-trip JSON. + void SaveSettings(json& o_json); + void LoadSettings(const json& o_json); + void RestoreDefaultSettings(); + void ClearShaderCache(); + // Called from Upscaling::PostPostLoad to seed subrect presets. + void PostPostLoad(); + + bool IsRuntimeSupported() const; + bool IsActive() const; + bool IsLoaded() const { return enabledAtBoot; } + + // Main enable: latched at boot, change requires restart + void LatchEnabled() { enabledAtBoot = (settings.enabled != 0); } + + // Quality mode reads through Upscaling::Settings — latch the boot value so + // downstream RT allocations stay coherent if the user moves the slider. + void LatchQualityMode(); + uint GetQualityModeAtBoot() const { return qualityModeAtBoot; } + + /// Render-to-display scale denominator for a quality mode index + /// (1=Quality .. 4=UltraPerformance). Delegates to the FFX SDK ratio table. + static float GetRenderScaleForQuality(uint qualityMode); + + DlssMode GetDlssMode() const { return (DlssMode)std::min(settings.dlssMode, 1u); } + StretchMode GetStretchMode() const { return (StretchMode)std::min(settings.stretchMode, 2u); } + + // Active getters: clamp + route shared fields through Upscaling::Settings. + uint GetActiveQualityMode() const; + uint GetActivePresetDLSS() const; + float GetActiveSharpnessDLSS() const; + + // Re-clamp cross-feature settings (preset vs DLSS mode). Idempotent; safe to call + // from Upscaling::LoadSettings after JSON has overwritten shared fields. + void ClampSettings(); + +private: + bool enabledAtBoot = false; // latched from settings.enabled at boot + uint qualityModeAtBoot = 4; // latched from Upscaling::Settings::qualityMode at boot + + bool IsPresetCompatibleWithMode(uint presetIndex) const; + void ClampPresetToMode(); +}; diff --git a/src/Features/Upscaling/FoveatedRender/Bridge.cpp b/src/Features/Upscaling/FoveatedRender/Bridge.cpp new file mode 100644 index 0000000000..bc4309c9df --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Bridge.cpp @@ -0,0 +1,79 @@ +#include "Bridge.h" + +#include "../../../Globals.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +bool FoveatedRenderImpl::Bridge::IsRouteActive() +{ + // IsActive() already checks: globals::game::isVR + // && globals::features::upscaling.streamline.featureDLSS + // && enabledAtBoot + return globals::features::upscaling.foveatedRender.IsActive(); +} + +// Bridge.h contract: when the route is inactive, getters return a neutral / +// identity value so callers that forget to check IsRouteActive() don't +// silently pick up FoveatedRender values. + +uint32_t FoveatedRenderImpl::Bridge::GetQualityMode() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetActiveQualityMode(); +} + +uint32_t FoveatedRenderImpl::Bridge::GetPresetDLSS() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetActivePresetDLSS(); +} + +float FoveatedRenderImpl::Bridge::GetSharpnessDLSS() +{ + if (!IsRouteActive()) + return 0.0f; + return globals::features::upscaling.foveatedRender.GetActiveSharpnessDLSS(); +} + +void FoveatedRenderImpl::Bridge::BootSequence() +{ + auto& enhancer = globals::features::upscaling.foveatedRender; + enhancer.LatchEnabled(); + enhancer.LatchQualityMode(); +} + +void FoveatedRenderImpl::Bridge::ComputeMvecScale(float& outX, float& outY) +{ + // Default: identity (caller's normal Streamline path). + outX = 1.0f; + outY = 1.0f; + + if (!IsRouteActive()) + return; + + auto& enhancer = globals::features::upscaling.foveatedRender; + const auto& uv = enhancer.subrectController.GetUV(); // PR-1 stereo Subrect: GetUV() == left-eye in stereo mode + const bool isFullEye = (uv.w >= 0.999f && uv.h >= 0.999f); + + if (isFullEye) + return; + + // Default + Faster both use per-eye DLSS calls (not strip-merged), so + // motion vectors scale by 1/UV.w on x. + outX = (uv.w > 0.0f) ? (1.0f / uv.w) : 1.0f; + outY = (uv.h > 0.0f) ? (1.0f / uv.h) : 1.0f; +} + +float FoveatedRenderImpl::Bridge::GetRenderScaleForQuality(uint32_t qualityMode) +{ + return FoveatedRender::GetRenderScaleForQuality(qualityMode); +} + +uint32_t FoveatedRenderImpl::Bridge::GetQualityModeAtBoot() +{ + if (!IsRouteActive()) + return 0u; + return globals::features::upscaling.foveatedRender.GetQualityModeAtBoot(); +} diff --git a/src/Features/Upscaling/FoveatedRender/Bridge.h b/src/Features/Upscaling/FoveatedRender/Bridge.h new file mode 100644 index 0000000000..dd408dfc2c --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Bridge.h @@ -0,0 +1,42 @@ +#pragma once + +// FoveatedRenderImpl::Bridge — single point of contact between the FoveatedRender +// subsystem and the rest of Community Shaders (Upscaling, Streamline). +// +// All "is FoveatedRender active?", "what settings should DLSS use?", and +// "what happened at boot?" questions are answered here, so consumers never +// need to #include FoveatedRender.h or poke globals::features::upscaling.foveatedRender +// directly. +// +// IMPORTANT: when the FoveatedRender route is inactive every query returns a +// neutral / identity value — callers must still check IsRouteActive() and +// fall back to their own settings when it returns false. + +#include + +namespace FoveatedRenderImpl::Bridge +{ + // True when VR + DLSS available + FoveatedRender enabled-at-boot. + bool IsRouteActive(); + + // Settings forwarding (live values from FoveatedRender GUI). + uint32_t GetQualityMode(); + uint32_t GetPresetDLSS(); + float GetSharpnessDLSS(); + + // Boot-time latches. Run once during BSShaderRenderTargets::Create. + // Latches enable + qualityMode so settings cannot drift mid-frame. + void BootSequence(); + + // Compute motion-vector scale for Streamline constants. + // Returns {1,1} when route is inactive or subrect is full-eye. + void ComputeMvecScale(float& outX, float& outY); + + // Render-to-display scale for a quality mode index (1=Quality .. 4=UltraPerf). + // Delegates to the FFX SDK ratio table. + float GetRenderScaleForQuality(uint32_t qualityMode); + + // Quality mode latched at boot (resource sizing decisions consult this so + // they don't shift mid-game when the user changes the live setting). + uint32_t GetQualityModeAtBoot(); +} diff --git a/src/Features/Upscaling/FoveatedRender/Core.cpp b/src/Features/Upscaling/FoveatedRender/Core.cpp new file mode 100644 index 0000000000..78bd1ab1d3 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Core.cpp @@ -0,0 +1,581 @@ +#include "Core.h" +#include "Ops.h" + +#include "../../../State.h" +#include "../../../Util.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +#include + +namespace FoveatedRenderImpl::Ops +{ + // Mirrors the StretchCB layout in SubrectStretchCS.hlsl — 8 dims + mode + + // blur radius + debug flag + pad. Kept at namespace scope so the create-CB + // path can size against sizeof(StretchCB) instead of a magic number. + struct StretchCB + { + uint32_t data[8]; + uint32_t stretchMode; + float blurRadius; + uint32_t debugVisualize; + uint32_t pad; + }; + + eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, + bool copyBindFlags, bool createSRV, bool createUAV, const char* name) + { + if (!src) { + logger::error("[FOVEATED] CreateTextureFromSource called with null src ({})", name ? name : ""); + return nullptr; + } + + // QueryInterface for ID3D11Texture2D rather than blind static_cast — a + // non-texture resource passed here would crash GetDesc otherwise. + // (CodeRabbit on PR #44.) + winrt::com_ptr srcTex; + if (FAILED(src->QueryInterface(IID_PPV_ARGS(srcTex.put())))) { + logger::error("[FOVEATED] CreateTextureFromSource src is not an ID3D11Texture2D ({})", name ? name : ""); + return nullptr; + } + + D3D11_TEXTURE2D_DESC srcDesc; + srcTex->GetDesc(&srcDesc); + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = srcDesc.Format; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = copyBindFlags ? srcDesc.BindFlags : (D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + + auto tex = eastl::make_unique(desc); + + if (name) { + Util::SetResourceName(tex->resource.get(), name); + } + + if (createSRV) { + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = srcDesc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + tex->CreateSRV(srvDesc); + } + + if (createUAV) { + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {}; + uavDesc.Format = srcDesc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + uavDesc.Texture2D.MipSlice = 0; + tex->CreateUAV(uavDesc); + } + + return tex; + } + + void EnsureVRIntermediateTextures( + uint32_t inWidth, + uint32_t inHeight, + uint32_t outWidth, + uint32_t outHeight, + ID3D11Resource* colorSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc) + { + bool needsRecreate = !Core::vrIntermediateColorIn[0] || !Core::vrIntermediateColorOut[0]; + if (!needsRecreate) { + needsRecreate = (Core::vrIntermediateColorIn[0]->desc.Width != inWidth || + Core::vrIntermediateColorIn[0]->desc.Height != inHeight || + Core::vrIntermediateColorOut[0]->desc.Width != outWidth || + Core::vrIntermediateColorOut[0]->desc.Height != outHeight); + } + // Recreate if reactive/transparency source appeared but intermediate is missing + if (!needsRecreate) { + needsRecreate = (reactiveSrc && !Core::vrIntermediateReactiveMask[0]) || + (transparencySrc && !Core::vrIntermediateTransparencyMask[0]); + } + + // Also reset stale intermediates when a source DISAPPEARED. Otherwise + // the per-eye reactive/transparency intermediates keep their last-known + // data and PreparePerEyeInputs's null-source branch skips the copy — + // DLSS then samples stale masks. Drop the intermediate so subsequent + // frames don't read it. Independent of the recreate path: shrinking is + // cheap and the next non-null source will trigger recreate above. + // (Copilot on PR #44.) + if (!reactiveSrc && Core::vrIntermediateReactiveMask[0]) { + Core::vrIntermediateReactiveMask[0].reset(); + Core::vrIntermediateReactiveMask[1].reset(); + } + if (!transparencySrc && Core::vrIntermediateTransparencyMask[0]) { + Core::vrIntermediateTransparencyMask[0].reset(); + Core::vrIntermediateTransparencyMask[1].reset(); + } + + if (!needsRecreate) { + return; + } + + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + + Core::vrIntermediateColorIn[i] = CreateTextureFromSource(colorSrc, inWidth, inHeight, false, true, true, ("FoveatedRender_ColorIn_" + suffix).c_str()); + Core::vrIntermediateColorOut[i] = CreateTextureFromSource(colorSrc, outWidth, outHeight, false, true, false, ("FoveatedRender_ColorOut_" + suffix).c_str()); + + D3D11_TEXTURE2D_DESC depthDesc = {}; + depthDesc.Width = inWidth; + depthDesc.Height = inHeight; + depthDesc.MipLevels = 1; + depthDesc.ArraySize = 1; + depthDesc.Format = DXGI_FORMAT_R32_TYPELESS; + depthDesc.SampleDesc.Count = 1; + depthDesc.Usage = D3D11_USAGE_DEFAULT; + depthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + Core::vrIntermediateDepth[i] = eastl::make_unique(depthDesc); + Util::SetResourceName(Core::vrIntermediateDepth[i]->resource.get(), ("FoveatedRender_Depth_" + suffix).c_str()); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R32_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + Core::vrIntermediateDepth[i]->CreateSRV(srvDesc); + + Core::vrIntermediateMotionVectors[i] = CreateTextureFromSource(mvecSrc, inWidth, inHeight, false, true, false, ("FoveatedRender_MVec_" + suffix).c_str()); + if (reactiveSrc) + Core::vrIntermediateReactiveMask[i] = CreateTextureFromSource(reactiveSrc, inWidth, inHeight, false, true, false, ("FoveatedRender_Reactive_" + suffix).c_str()); + else + Core::vrIntermediateReactiveMask[i].reset(); + if (transparencySrc) + Core::vrIntermediateTransparencyMask[i] = CreateTextureFromSource(transparencySrc, inWidth, inHeight, false, true, false, ("FoveatedRender_Transparency_" + suffix).c_str()); + else + Core::vrIntermediateTransparencyMask[i].reset(); + } + } + + void EnsureVRSubrectTextures( + uint32_t subInW, + uint32_t subInH, + uint32_t subOutW, + uint32_t subOutH, + ID3D11Resource* colorSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc) + { + bool needsRecreate = !Core::vrSubrectColorIn[0] || + Core::vrSubrectInW != subInW || Core::vrSubrectInH != subInH || + Core::vrSubrectOutW != subOutW || Core::vrSubrectOutH != subOutH; + // Recreate if reactive/transparency source appeared but intermediate is missing + if (!needsRecreate) { + needsRecreate = (reactiveSrc && !Core::vrSubrectReactiveMask[0]) || + (transparencySrc && !Core::vrSubrectTransparencyMask[0]); + } + + if (needsRecreate) { + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + Core::vrSubrectColorIn[i] = CreateTextureFromSource(colorSrc, subInW, subInH, false, true, true, ("FoveatedRender_Subrect_ColorIn_" + suffix).c_str()); + Core::vrSubrectColorOut[i] = CreateTextureFromSource(colorSrc, subOutW, subOutH, false, true, false, ("FoveatedRender_Subrect_ColorOut_" + suffix).c_str()); + + D3D11_TEXTURE2D_DESC depthDesc = {}; + depthDesc.Width = subInW; + depthDesc.Height = subInH; + depthDesc.MipLevels = 1; + depthDesc.ArraySize = 1; + depthDesc.Format = DXGI_FORMAT_R32_TYPELESS; + depthDesc.SampleDesc.Count = 1; + depthDesc.Usage = D3D11_USAGE_DEFAULT; + depthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + Core::vrSubrectDepth[i] = eastl::make_unique(depthDesc); + Util::SetResourceName(Core::vrSubrectDepth[i]->resource.get(), ("FoveatedRender_Subrect_Depth_" + suffix).c_str()); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = DXGI_FORMAT_R32_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + Core::vrSubrectDepth[i]->CreateSRV(srvDesc); + + Core::vrSubrectMotionVectors[i] = CreateTextureFromSource(mvecSrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_MVec_" + suffix).c_str()); + if (reactiveSrc) + Core::vrSubrectReactiveMask[i] = CreateTextureFromSource(reactiveSrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_Reactive_" + suffix).c_str()); + else + Core::vrSubrectReactiveMask[i].reset(); + if (transparencySrc) + Core::vrSubrectTransparencyMask[i] = CreateTextureFromSource(transparencySrc, subInW, subInH, false, true, false, ("FoveatedRender_Subrect_Transparency_" + suffix).c_str()); + else + Core::vrSubrectTransparencyMask[i].reset(); + } + + Core::vrSubrectInW = subInW; + Core::vrSubrectInH = subInH; + Core::vrSubrectOutW = subOutW; + Core::vrSubrectOutH = subOutH; + } + } + + bool PreparePerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + // Required sources are dereferenced unconditionally below; bail + // rather than null-deref CopySubresourceRegion. Reactive/transparency + // are optional and already conditionally copied. + if (!colorSrc || !depthSrc || !mvecSrc) { + logger::error("[FOVEATED] PreparePerEyeInputs missing required source textures"); + return false; + } + + EnsureVRIntermediateTextures( + eyeWidthIn, + eyeHeightIn, + eyeWidthOut, + eyeHeightOut, + colorSrc, + mvecSrc, + reactiveSrc, + transparencySrc); + + for (uint32_t i = 0; i < 2; ++i) { + if (!Core::vrIntermediateColorIn[i] || !Core::vrIntermediateColorOut[i] || + !Core::vrIntermediateDepth[i] || !Core::vrIntermediateMotionVectors[i] || + (reactiveSrc && !Core::vrIntermediateReactiveMask[i]) || + (transparencySrc && !Core::vrIntermediateTransparencyMask[i])) { + logger::error("[FOVEATED] Missing per-eye intermediate resources for eye {}", i); + return false; + } + } + + auto context = globals::d3d::context; + auto* depthSRV = globals::game::renderer->GetDepthStencilData() + .depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN] + .depthSRV; + for (uint32_t i = 0; i < 2; ++i) { + uint32_t offsetXIn = (i == 1) ? eyeWidthIn : 0; + D3D11_BOX srcBox = { offsetXIn, 0, 0, offsetXIn + eyeWidthIn, eyeHeightIn, 1 }; + + context->CopySubresourceRegion(Core::vrIntermediateColorIn[i]->resource.get(), 0, 0, 0, 0, colorSrc, 0, &srcBox); + context->CopySubresourceRegion(Core::vrIntermediateDepth[i]->resource.get(), 0, 0, 0, 0, depthSrc, 0, &srcBox); + context->CopySubresourceRegion(Core::vrIntermediateMotionVectors[i]->resource.get(), 0, 0, 0, 0, mvecSrc, 0, &srcBox); + if (transparencySrc) + context->CopySubresourceRegion(Core::vrIntermediateTransparencyMask[i]->resource.get(), 0, 0, 0, 0, transparencySrc, 0, &srcBox); + if (reactiveSrc) + context->CopySubresourceRegion(Core::vrIntermediateReactiveMask[i]->resource.get(), 0, 0, 0, 0, reactiveSrc, 0, &srcBox); + + // Reapply HMD hidden-area mask clear into the per-eye intermediate so DLSS + // history doesn't accumulate garbage from the masked-out region. + // Depth source is full SBS (read at per-eye offset); color destination is per-eye + // sized (write at offset 0). + globals::features::upscaling.ClearHMDMask( + Core::vrIntermediateColorIn[i]->uav.get(), + depthSRV, + eyeWidthIn, + eyeHeightIn, + i * eyeWidthIn, + 0); + } + + return true; + } + + bool FinalizePerEyeOutputs(ID3D11Resource* colorDst, uint32_t eyeWidthOut, uint32_t eyeHeightOut) + { + if (!colorDst) { + logger::error("[FOVEATED] FinalizePerEyeOutputs received null destination color resource"); + return false; + } + + for (uint32_t i = 0; i < 2; ++i) { + if (!Core::vrIntermediateColorOut[i]) { + logger::error("[FOVEATED] Missing per-eye output resource for eye {}", i); + return false; + } + } + + auto context = globals::d3d::context; + for (uint32_t i = 0; i < 2; ++i) { + uint32_t offsetXOut = (i == 1) ? eyeWidthOut : 0; + D3D11_BOX outBox = { 0, 0, 0, eyeWidthOut, eyeHeightOut, 1 }; + context->CopySubresourceRegion(colorDst, 0, offsetXOut, 0, 0, Core::vrIntermediateColorOut[i]->resource.get(), 0, &outBox); + } + + return true; + } + + void StretchDRSToFullEye( + ID3D11ShaderResourceView* renderSBSSRV, + ID3D11UnorderedAccessView* kMainUAV, + uint32_t dstOffsetX, + uint32_t dstWidth, + uint32_t dstHeight, + uint32_t srcOffsetX, + uint32_t srcWidth, + uint32_t srcHeight, + uint32_t srcEyeWidth, + uint32_t srcEyeHeight) + { + auto context = globals::d3d::context; + + if (!Core::vrSubrectStretchCS) { + Core::vrSubrectStretchCS.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/FoveatedRender/SubrectStretchCS.hlsl", {}, "cs_5_0")); + Util::SetResourceName(Core::vrSubrectStretchCS.get(), "FoveatedRender::SubrectStretchCS"); + + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.ByteWidth = sizeof(StretchCB); + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(globals::d3d::device->CreateBuffer(&cbDesc, nullptr, Core::vrSubrectStretchCB.put()))) { + logger::error("[FOVEATED] Failed to create SubrectStretch constant buffer"); + // Drop the partially-attached CS so the next frame retries the + // whole init block — otherwise the outer !vrSubrectStretchCS + // guard above stays false forever and Faster mode is dead for + // the rest of the session. + Core::vrSubrectStretchCS = nullptr; + return; + } + Util::SetResourceName(Core::vrSubrectStretchCB.get(), "FoveatedRender::SubrectStretchCB"); + + D3D11_SAMPLER_DESC sampDesc = {}; + sampDesc.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT; + sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + if (FAILED(globals::d3d::device->CreateSamplerState(&sampDesc, Core::vrSubrectStretchSampler.put()))) { + logger::error("[FOVEATED] Failed to create SubrectStretch sampler"); + Core::vrSubrectStretchCS = nullptr; + Core::vrSubrectStretchCB = nullptr; + return; + } + Util::SetResourceName(Core::vrSubrectStretchSampler.get(), "FoveatedRender::SubrectStretchSampler"); + } + + if (!Core::vrSubrectStretchCS || !Core::vrSubrectStretchCB || !Core::vrSubrectStretchSampler) { + return; + } + + // Guard against a null destination UAV — CSSetUnorderedAccessViews + + // Dispatch with nullptr would either no-op silently or assert in + // debug builds. Returning lets the route's `routeHandled=false` path + // fall back to standard DLSS so users still see output. (CodeRabbit + // on PR #44.) + if (!kMainUAV) { + logger::error("[FOVEATED] StretchDRSToFullEye called with null kMainUAV"); + return; + } + + D3D11_MAPPED_SUBRESOURCE mapped{}; + // If Map fails the constant buffer keeps stale data from a prior + // dispatch. CSSetConstantBuffers + Dispatch would then run the + // shader against stale geometry/scale parameters, producing wrong + // pixels rather than no pixels. Early-return preserves the prior + // frame's output. (CodeRabbit on PR #44.) + if (FAILED(context->Map(Core::vrSubrectStretchCB.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + logger::error("[FOVEATED] StretchDRSToFullEye Map(vrSubrectStretchCB) failed; skipping dispatch"); + return; + } + { + auto& enhSettings = globals::features::upscaling.foveatedRender.settings; + StretchCB cb = {}; + cb.data[0] = dstOffsetX; + cb.data[1] = dstWidth; + cb.data[2] = dstHeight; + cb.data[3] = srcOffsetX; + cb.data[4] = srcWidth; + cb.data[5] = srcHeight; + cb.data[6] = srcEyeWidth; + cb.data[7] = srcEyeHeight; + cb.stretchMode = enhSettings.stretchMode; + // Fixed 1.0 blur radius for the GaussianBlur stretch path. + cb.blurRadius = 1.0f; + cb.debugVisualize = enhSettings.debugVisualize; + std::memcpy(mapped.pData, &cb, sizeof(cb)); + context->Unmap(Core::vrSubrectStretchCB.get(), 0); + } + + context->CSSetShader(Core::vrSubrectStretchCS.get(), nullptr, 0); + ID3D11Buffer* cbs[1] = { Core::vrSubrectStretchCB.get() }; + context->CSSetConstantBuffers(0, 1, cbs); + ID3D11ShaderResourceView* srvs[1] = { renderSBSSRV }; + context->CSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samplers[1] = { Core::vrSubrectStretchSampler.get() }; + context->CSSetSamplers(0, 1, samplers); + ID3D11UnorderedAccessView* uavs[1] = { kMainUAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + + context->Dispatch((dstWidth + 7) / 8, (dstHeight + 7) / 8, 1); + + ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; + ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; + ID3D11Buffer* nullCB[1] = { nullptr }; + ID3D11SamplerState* nullSampler[1] = { nullptr }; + context->CSSetShaderResources(0, 1, nullSRV); + context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); + context->CSSetConstantBuffers(0, 1, nullCB); + context->CSSetSamplers(0, 1, nullSampler); + context->CSSetShader(nullptr, nullptr, 0); + } + + void EnsureVRRenderSBS(uint32_t renderW, uint32_t renderH, ID3D11Resource* colorSrc) + { + if (!Core::vrRenderSBS || Core::vrRenderSBSW != renderW || Core::vrRenderSBSH != renderH) { + // UAV is required for the Faster-mode HMD mask clear pass (ClearHMDMask + // writes through the UAV before DLSS reads the SBS via extent offsets). + Core::vrRenderSBS = CreateTextureFromSource(colorSrc, renderW, renderH, false, true, true, "FoveatedRender_RenderSBS"); + Core::vrRenderSBSW = renderW; + Core::vrRenderSBSH = renderH; + } + } + + void EnsureFasterOutputTextures(uint32_t subOutW, uint32_t subOutH, ID3D11Resource* colorSrc) + { + bool needsRecreate = !Core::vrFasterColorOut[0] || + Core::vrFasterOutW != subOutW || Core::vrFasterOutH != subOutH; + if (!needsRecreate) + return; + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + Core::vrFasterColorOut[i] = CreateTextureFromSource(colorSrc, subOutW, subOutH, false, true, false, ("FoveatedRender_Faster_ColorOut_" + suffix).c_str()); + } + Core::vrFasterOutW = subOutW; + Core::vrFasterOutH = subOutH; + } + + uint64_t ComputeSubrectUVHash(const Util::Subrect::UVRegion& leftUV, + const Util::Subrect::UVRegion& rightUV, uint32_t mode) + { + uint64_t h = 0; + auto mix = [&](uint64_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 12) + (h >> 4); }; + auto mixUV = [&](const Util::Subrect::UVRegion& uv) { + mix(std::hash{}(uv.x)); + mix(std::hash{}(uv.y)); + mix(std::hash{}(uv.w)); + mix(std::hash{}(uv.h)); + }; + mixUV(leftUV); + mixUV(rightUV); + mix(std::hash{}(mode)); + return h; + } + + void SnapshotSBS(ID3D11Resource* src, uint32_t renderW, uint32_t renderH) + { + EnsureVRRenderSBS(renderW, renderH, src); + auto context = globals::d3d::context; + D3D11_BOX drsBox = { 0, 0, 0, renderW, renderH, 1 }; + context->CopySubresourceRegion(Core::vrRenderSBS->resource.get(), 0, 0, 0, 0, src, 0, &drsBox); + } + + void StretchDRSBothEyes(ID3D11UnorderedAccessView* dstUAV, uint32_t eyeWidthOut, uint32_t eyeHeightOut, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t renderW, uint32_t renderH, + ID3D11ShaderResourceView* srcOverride) + { + // Snapshot creation can fail or be skipped on a fresh frame; degrade + // rather than dereference vrRenderSBS->srv on the null path. + auto* src = srcOverride ? srcOverride : + (Core::vrRenderSBS ? Core::vrRenderSBS->srv.get() : nullptr); + if (!src) { + logger::error("[FOVEATED] StretchDRSBothEyes missing source SRV"); + return; + } + for (uint32_t i = 0; i < 2; ++i) { + uint32_t dstX = (i == 1) ? eyeWidthOut : 0; + uint32_t srcX = (i == 1) ? eyeWidthIn : 0; + StretchDRSToFullEye( + src, dstUAV, + dstX, eyeWidthOut, eyeHeightOut, + srcX, renderW, renderH, + eyeWidthIn, eyeHeightIn); + } + } + + void BlendSubrectToOutput(ID3D11Resource* dlssSrc, ID3D11Resource* dst, + uint32_t dstOffsetX, uint32_t dstOffsetY, uint32_t subWidth, uint32_t subHeight, uint32_t srcOffsetX) + { + auto context = globals::d3d::context; + D3D11_BOX srcBox = { srcOffsetX, 0, 0, srcOffsetX + subWidth, subHeight, 1 }; + context->CopySubresourceRegion(dst, 0, dstOffsetX, dstOffsetY, 0, dlssSrc, 0, &srcBox); + } + +} // namespace FoveatedRenderImpl::Ops + +namespace FoveatedRenderImpl +{ + bool Core::PrepareVRPerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + return Ops::PreparePerEyeInputs( + colorSrc, + depthSrc, + mvecSrc, + reactiveSrc, + transparencySrc, + eyeWidthIn, + eyeHeightIn, + eyeWidthOut, + eyeHeightOut); + } + + bool Core::FinalizeVRPerEyeOutputs( + ID3D11Resource* colorDst, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut) + { + return Ops::FinalizePerEyeOutputs(colorDst, eyeWidthOut, eyeHeightOut); + } + + void Core::ClearResources() + { + for (int i = 0; i < 2; ++i) { + vrIntermediateColorIn[i].reset(); + vrIntermediateColorOut[i].reset(); + vrIntermediateDepth[i].reset(); + vrIntermediateMotionVectors[i].reset(); + vrIntermediateReactiveMask[i].reset(); + vrIntermediateTransparencyMask[i].reset(); + + vrSubrectColorIn[i].reset(); + vrSubrectColorOut[i].reset(); + vrSubrectDepth[i].reset(); + vrSubrectMotionVectors[i].reset(); + vrSubrectReactiveMask[i].reset(); + vrSubrectTransparencyMask[i].reset(); + } + vrSubrectInW = vrSubrectInH = vrSubrectOutW = vrSubrectOutH = 0; + + vrRenderSBS.reset(); + vrRenderSBSW = vrRenderSBSH = 0; + + vrFasterColorOut[0].reset(); + vrFasterColorOut[1].reset(); + vrFasterOutW = vrFasterOutH = 0; + + activeSubrectUVHash = 0; + } + + void Core::ClearShaderCache() + { + vrSubrectStretchCS = nullptr; + vrSubrectStretchCB = nullptr; + vrSubrectStretchSampler = nullptr; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Core.h b/src/Features/Upscaling/FoveatedRender/Core.h new file mode 100644 index 0000000000..7b4a04e869 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Core.h @@ -0,0 +1,93 @@ +#pragma once + +// ============================================================================ +// FoveatedRenderImpl::Core — GPU resource pool & mode-dispatch entry point +// ============================================================================ +// +// Owns all per-mode intermediate textures (Default / Faster), compute-shader +// objects (subrect stretch), and the public entry points consumed by +// Upscaling.cpp. +// +// ============================================================================ + +#include "Buffer.h" +#include "Params.h" +#include +#include + +class Streamline; + +namespace FoveatedRenderImpl +{ + class Core + { + public: + // Stage1: dispatches across Default / Faster modes. + static bool ExecuteVRDlssCore(Streamline& streamline, + ID3D11Resource* upscalingTexture, + ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, + ID3D11Resource* transparencyMask, + ID3D11Resource* motionVectors); + + // Shared VR per-eye preprocessing/finalization for non-DLSS callers (e.g. FSR). + static bool PrepareVRPerEyeInputs( + ID3D11Resource* colorSrc, + ID3D11Resource* depthSrc, + ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, + ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, + uint32_t eyeHeightIn, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut); + + static bool FinalizeVRPerEyeOutputs( + ID3D11Resource* colorDst, + uint32_t eyeWidthOut, + uint32_t eyeHeightOut); + + // Release all GPU resources owned by Core. + static void ClearResources(); + static void ClearShaderCache(); + + // ── Own VR resources (independent from Upscaling) ── + + // Per-eye intermediate buffers (Default full-eye mode) + static inline eastl::unique_ptr vrIntermediateColorIn[2]; + static inline eastl::unique_ptr vrIntermediateColorOut[2]; + static inline eastl::unique_ptr vrIntermediateDepth[2]; + static inline eastl::unique_ptr vrIntermediateMotionVectors[2]; + static inline eastl::unique_ptr vrIntermediateReactiveMask[2]; + static inline eastl::unique_ptr vrIntermediateTransparencyMask[2]; + + // Subrect-sized textures (Default/Faster subrect mode) + static inline eastl::unique_ptr vrSubrectColorIn[2]; + static inline eastl::unique_ptr vrSubrectColorOut[2]; + static inline eastl::unique_ptr vrSubrectDepth[2]; + static inline eastl::unique_ptr vrSubrectMotionVectors[2]; + static inline eastl::unique_ptr vrSubrectReactiveMask[2]; + static inline eastl::unique_ptr vrSubrectTransparencyMask[2]; + static inline uint32_t vrSubrectInW = 0, vrSubrectInH = 0, vrSubrectOutW = 0, vrSubrectOutH = 0; + + // Faster mode per-eye output textures (subOutW × subOutH) + static inline eastl::unique_ptr vrFasterColorOut[2]; + static inline uint32_t vrFasterOutW = 0, vrFasterOutH = 0; + + // DRS region copy (render-resolution SBS) + static inline eastl::unique_ptr vrRenderSBS; + static inline uint32_t vrRenderSBSW = 0, vrRenderSBSH = 0; + + // DRS stretch compute shader resources + static inline winrt::com_ptr vrSubrectStretchCS; + static inline winrt::com_ptr vrSubrectStretchCB; + static inline winrt::com_ptr vrSubrectStretchSampler; + + // Subrect UV hash for resource recreation detection + static inline uint64_t activeSubrectUVHash = 0; + + private: + static bool ExecuteDefaultMode(Streamline& streamline, const VRDlssParams& p); + static bool ExecuteFasterMode(Streamline& streamline, const VRDlssParams& p); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Modes.cpp b/src/Features/Upscaling/FoveatedRender/Modes.cpp new file mode 100644 index 0000000000..56628cc365 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Modes.cpp @@ -0,0 +1,254 @@ +// ============================================================================ +// Modes.cpp — Default / Faster DLSS execution strategies +// ============================================================================ +// +// Each mode composes Ops primitives (snapshot, stretch, crop, blend…) in a +// different order. Router resolves VRDlssParams and dispatches. +// +// ============================================================================ + +#include "Core.h" +#include "Ops.h" +#include "Params.h" + +#include "../../../Globals.h" +#include "../../../Utils/Subrect.h" +#include "../../Upscaling.h" +#include "../Streamline.h" + +namespace FoveatedRenderImpl +{ + using namespace Ops; + + // ── Router: resolves params via Params module, dispatches to the selected mode ── + + bool Core::ExecuteVRDlssCore(Streamline& streamline, + ID3D11Resource* upscalingTexture, ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, ID3D11Resource* motionVectors) + { + auto p = VRDlssParams::Resolve(upscalingTexture, depthTexture, reactiveMask, transparencyMask, motionVectors); + + // Detect UV/mode change → destroy DLSS resources so SL recreates them at + // the new size. Both eye UVs feed the hash; asymmetric presets (e.g. + // Nasal Convergence) can change rightUV while leftUV stays put. + uint64_t uvHash = ComputeSubrectUVHash(p.leftUV, p.rightUV, (uint32_t)p.mode); + if (uvHash != Core::activeSubrectUVHash) { + logger::info("[FOVEATED] Subrect UV or mode changed, recreating DLSS resources"); + streamline.DestroyDLSSResources(); + Core::activeSubrectUVHash = uvHash; + } + + switch (p.mode) { + case FoveatedRender::DlssMode::kFaster: + return ExecuteFasterMode(streamline, p); + default: + return ExecuteDefaultMode(streamline, p); + } + } + + // ── Default mode: per-eye isolation, 2 resource sets, 2 evaluates ── + + bool Core::ExecuteDefaultMode(Streamline& streamline, const VRDlssParams& p) + { + // Subrect path needs colorDstUAV (StretchDRSBothEyes writes through it). + // Full-eye path doesn't touch it. Return false on the subrect path so + // the router falls back to standard DLSS rather than hitting the null + // guard inside StretchDRSToFullEye every frame. (CodeRabbit on PR #44.) + if (!p.isFullEye && !p.colorDstUAV) { + logger::error("[FOVEATED] ExecuteDefaultMode subrect path missing colorDstUAV — falling back"); + return false; + } + if (p.isFullEye) { + // Full-eye path: same as standard VR DLSS + if (!PreparePerEyeInputs( + p.colorSrc, p.depthTexture, p.motionVectors, p.reactiveMask, p.transparencyMask, + p.eyeWidthIn, p.eyeHeightIn, p.eyeWidthOut, p.eyeHeightOut)) + return false; + + for (uint32_t i = 0; i < 2; ++i) { + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ 0, 0, p.eyeWidthIn, p.eyeHeightIn }; + sl::Extent extentOut{ 0, 0, p.eyeWidthOut, p.eyeHeightOut }; + streamline.EvaluateDLSS(vp, i, + Core::vrIntermediateColorIn[i]->resource.get(), Core::vrIntermediateColorOut[i]->resource.get(), + Core::vrIntermediateDepth[i]->resource.get(), Core::vrIntermediateMotionVectors[i]->resource.get(), + p.reactiveMask ? Core::vrIntermediateReactiveMask[i]->resource.get() : nullptr, + p.transparencyMask ? Core::vrIntermediateTransparencyMask[i]->resource.get() : nullptr, + extentIn, extentOut, p.eyeWidthOut); + } + + return FinalizePerEyeOutputs(p.colorDst, p.eyeWidthOut, p.eyeHeightOut); + } + + // ── Subrect path: crop per-eye, DLSS at subrect size, stretch back ── + + const Util::Subrect::UVRegion* eyeUVs[2] = { &p.leftUV, &p.rightUV }; + + // NOTE: EnsureVRSubrectTextures allocates a single shared per-eye texture + // set sized to LEFT-eye subrect dimensions. Correct only while + // Util::Subrect's auto-mirror keeps leftUV.w/h == rightUV.w/h — the + // per-eye loop below uses the eye's own uv for the real extents. + uint32_t allocSubInW = std::max(1, (uint32_t)(p.eyeWidthIn * p.leftUV.w)); + uint32_t allocSubInH = std::max(1, (uint32_t)(p.eyeHeightIn * p.leftUV.h)); + uint32_t allocSubOutW = std::max(1, (uint32_t)(p.eyeWidthOut * p.leftUV.w)); + uint32_t allocSubOutH = std::max(1, (uint32_t)(p.eyeHeightOut * p.leftUV.h)); + + EnsureVRSubrectTextures(allocSubInW, allocSubInH, allocSubOutW, allocSubOutH, + p.colorSrc, p.motionVectors, p.reactiveMask, p.transparencyMask); + + // Snapshot + Stretch DRS → kMAIN (fill full-eye background) + SnapshotSBS(p.colorSrc, p.renderW, p.renderH); + + StretchDRSBothEyes(p.colorDstUAV, p.eyeWidthOut, p.eyeHeightOut, p.eyeWidthIn, p.eyeHeightIn, p.renderW, p.renderH); + + // Crop subrect per-eye from snapshot (not kMAIN which was overwritten by stretch) + auto context = globals::d3d::context; + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing — right eye uses rightUV.w/h, not leftUV. + uint32_t subInW = std::max(1, (uint32_t)(p.eyeWidthIn * uv.w)); + uint32_t subInH = std::max(1, (uint32_t)(p.eyeHeightIn * uv.h)); + uint32_t subOutW = std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t cropX = (uint32_t)(uv.x * p.eyeWidthIn); + uint32_t cropY = (uint32_t)(uv.y * p.eyeHeightIn); + uint32_t sbsX = (i == 1 ? p.eyeWidthIn : 0) + cropX; + D3D11_BOX sbsCrop = { sbsX, cropY, 0, sbsX + subInW, cropY + subInH, 1 }; + + context->CopySubresourceRegion(Core::vrSubrectColorIn[i]->resource.get(), 0, 0, 0, 0, Core::vrRenderSBS->resource.get(), 0, &sbsCrop); + context->CopySubresourceRegion(Core::vrSubrectDepth[i]->resource.get(), 0, 0, 0, 0, p.depthTexture, 0, &sbsCrop); + context->CopySubresourceRegion(Core::vrSubrectMotionVectors[i]->resource.get(), 0, 0, 0, 0, p.motionVectors, 0, &sbsCrop); + if (p.reactiveMask) + context->CopySubresourceRegion(Core::vrSubrectReactiveMask[i]->resource.get(), 0, 0, 0, 0, p.reactiveMask, 0, &sbsCrop); + if (p.transparencyMask) + context->CopySubresourceRegion(Core::vrSubrectTransparencyMask[i]->resource.get(), 0, 0, 0, 0, p.transparencyMask, 0, &sbsCrop); + + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ 0, 0, subInW, subInH }; + sl::Extent extentOut{ 0, 0, subOutW, subOutH }; + streamline.EvaluateDLSS(vp, i, + Core::vrSubrectColorIn[i]->resource.get(), Core::vrSubrectColorOut[i]->resource.get(), + Core::vrSubrectDepth[i]->resource.get(), Core::vrSubrectMotionVectors[i]->resource.get(), + p.reactiveMask ? Core::vrSubrectReactiveMask[i]->resource.get() : nullptr, + p.transparencyMask ? Core::vrSubrectTransparencyMask[i]->resource.get() : nullptr, + extentIn, extentOut, subOutW, subOutH); + } + + // Write DLSS output back at subrect position (with optional blend) + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subOutW = std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t dstCropX = (uint32_t)(uv.x * p.eyeWidthOut); + uint32_t dstCropY = (uint32_t)(uv.y * p.eyeHeightOut); + uint32_t dstX = (i == 1 ? p.eyeWidthOut : 0) + dstCropX; + BlendSubrectToOutput(Core::vrSubrectColorOut[i]->resource.get(), p.colorDst, + dstX, dstCropY, subOutW, subOutH); + } + + return true; + } + + // ── Faster mode: DLSS reads directly from SBS via extents, per-eye output, 2 evaluates ── + // Input: kMAIN/depth/mvec SBS textures using extent offsets (zero input copies). + // Output: per-eye independent textures with extent {0,0}. + // Flow: DLSS read → snapshot+stretch background → copy outputs back to kMAIN. + + bool Core::ExecuteFasterMode(Streamline& streamline, const VRDlssParams& p) + { + // Subrect path needs colorDstUAV (StretchDRSBothEyes writes through it + // in Step 3). Full-eye Faster skips Step 3 — don't reject it here just + // because the UAV isn't bound. + if (!p.isFullEye && !p.colorDstUAV) { + logger::error("[FOVEATED] ExecuteFasterMode subrect path missing colorDstUAV — falling back"); + return false; + } + const Util::Subrect::UVRegion* eyeUVs[2] = { &p.leftUV, &p.rightUV }; + + // NOTE: EnsureFasterOutputTextures allocates one per-eye texture set + // sized to LEFT-eye subrect dimensions. Correct only while Util::Subrect + // auto-mirror keeps leftUV.w/h == rightUV.w/h. Per-eye DLSS extents + // below use the eye's own uv. + uint32_t allocSubOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * p.leftUV.w)); + uint32_t allocSubOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * p.leftUV.h)); + + // Step 1: Ensure per-eye output textures + EnsureFasterOutputTextures(allocSubOutW, allocSubOutH, p.colorSrc); + + // Step 2a: Snapshot kMAIN into vrRenderSBS so we can clear the HMD + // hidden-area ring without writing to kMAIN itself. Without this clear + // DLSS's temporal accumulation drags Skyrim's default sky clear from + // the masked-out edge into the visible region on fast head motion — + // the standard Streamline path (Streamline.cpp) and Default mode both + // pre-clear via per-eye intermediates. + SnapshotSBS(p.colorSrc, p.renderW, p.renderH); + auto& upscaling = globals::features::upscaling; + auto* depthSRV = globals::game::renderer->GetDepthStencilData() + .depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN] + .depthSRV; + if (Core::vrRenderSBS && Core::vrRenderSBS->uav && depthSRV) { + // Color target IS the SBS snapshot (not a per-eye buffer), so + // colorOffsetX must select the eye's half — same as depthOffsetX. + // ClearHMDMaskCS's default contract assumes the color target is + // per-eye (colorOffsetX = 0) and was written for Streamline's + // per-eye intermediates; here we're routing both eyes through one + // SBS texture so we override both offsets together. + for (uint32_t i = 0; i < 2; ++i) { + const uint32_t eyeOffsetX = i * p.eyeWidthIn; + upscaling.ClearHMDMask(Core::vrRenderSBS->uav.get(), depthSRV, + p.eyeWidthIn, p.eyeHeightIn, eyeOffsetX, eyeOffsetX); + } + } + ID3D11Resource* dlssColorSrc = (Core::vrRenderSBS ? Core::vrRenderSBS->resource.get() : p.colorSrc); + + // Step 2b: DLSS reads from the mask-cleared SBS snapshot via extent offsets + // → per-eye output. sl::Extent field order is {top, left, width, height}. + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subInW = p.isFullEye ? p.eyeWidthIn : std::max(1, (uint32_t)(p.eyeWidthIn * uv.w)); + uint32_t subInH = p.isFullEye ? p.eyeHeightIn : std::max(1, (uint32_t)(p.eyeHeightIn * uv.h)); + uint32_t subOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t cropX = p.isFullEye ? 0 : (uint32_t)(uv.x * p.eyeWidthIn); + uint32_t cropY = p.isFullEye ? 0 : (uint32_t)(uv.y * p.eyeHeightIn); + uint32_t inOffsetX = (i == 1 ? p.eyeWidthIn : 0) + cropX; + uint32_t inOffsetY = cropY; + + sl::ViewportHandle vp = (i == 1) ? streamline.viewportRight : streamline.viewport; + sl::Extent extentIn{ inOffsetY, inOffsetX, subInW, subInH }; + sl::Extent extentOut{ 0, 0, subOutW, subOutH }; + + streamline.EvaluateDLSS(vp, i, + dlssColorSrc, Core::vrFasterColorOut[i]->resource.get(), + p.depthTexture, p.motionVectors, + p.reactiveMask, p.transparencyMask, + extentIn, extentOut, subOutW, subOutH); + } + + // Step 3: Stretch DRS → kMAIN (subrect only) — snapshot reused from Step 2a. + if (!p.isFullEye) { + StretchDRSBothEyes(p.colorDstUAV, p.eyeWidthOut, p.eyeHeightOut, p.eyeWidthIn, p.eyeHeightIn, p.renderW, p.renderH); + } + + // Step 4: Copy DLSS output back (with optional blend) + for (uint32_t i = 0; i < 2; ++i) { + const auto& uv = *eyeUVs[i]; + // Per-eye sizing. + uint32_t subOutW = p.isFullEye ? p.eyeWidthOut : std::max(1, (uint32_t)(p.eyeWidthOut * uv.w)); + uint32_t subOutH = p.isFullEye ? p.eyeHeightOut : std::max(1, (uint32_t)(p.eyeHeightOut * uv.h)); + + uint32_t dstCropX = p.isFullEye ? 0 : (uint32_t)(uv.x * p.eyeWidthOut); + uint32_t dstCropY = p.isFullEye ? 0 : (uint32_t)(uv.y * p.eyeHeightOut); + uint32_t dstX = (i == 1 ? p.eyeWidthOut : 0) + dstCropX; + BlendSubrectToOutput(Core::vrFasterColorOut[i]->resource.get(), p.colorDst, + dstX, dstCropY, subOutW, subOutH); + } + + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Ops.h b/src/Features/Upscaling/FoveatedRender/Ops.h new file mode 100644 index 0000000000..44bcc8cd61 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Ops.h @@ -0,0 +1,66 @@ +#pragma once + +#include "Core.h" +#include "Utils/Subrect.h" + +#include +#include + +class Texture2D; + +// Primitive operations for the FoveatedRender VR DLSS pipeline. +// +// Each function is a self-contained building block. Mode pipelines in +// Modes.cpp compose these in different orders to form the Default and +// Faster strategies. +namespace FoveatedRenderImpl::Ops +{ + // Texture creation helper. + eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, + bool copyBindFlags = false, bool createSRV = false, bool createUAV = false, const char* name = nullptr); + + // Lazy/idempotent resource ensure helpers. + void EnsureVRIntermediateTextures(uint32_t inW, uint32_t inH, uint32_t outW, uint32_t outH, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + + void EnsureVRSubrectTextures(uint32_t subInW, uint32_t subInH, uint32_t subOutW, uint32_t subOutH, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + + void EnsureFasterOutputTextures(uint32_t subOutW, uint32_t subOutH, ID3D11Resource* colorSrc); + + void EnsureVRRenderSBS(uint32_t renderW, uint32_t renderH, ID3D11Resource* colorSrc); + + // Copy full-eye slices from SBS textures into per-eye intermediates. + bool PreparePerEyeInputs(ID3D11Resource* colorSrc, ID3D11Resource* depthSrc, ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t eyeWidthOut, uint32_t eyeHeightOut); + + // Copy per-eye output intermediates back into the SBS output texture. + bool FinalizePerEyeOutputs(ID3D11Resource* colorDst, uint32_t eyeWidthOut, uint32_t eyeHeightOut); + + // Snapshot kMAIN DRS data into vrRenderSBS. + void SnapshotSBS(ID3D11Resource* src, uint32_t renderW, uint32_t renderH); + + // Compute-shader stretch of a single eye region from renderSBS → kMAIN. + void StretchDRSToFullEye(ID3D11ShaderResourceView* renderSBSSRV, ID3D11UnorderedAccessView* kMainUAV, + uint32_t dstOffsetX, uint32_t dstWidth, uint32_t dstHeight, + uint32_t srcOffsetX, uint32_t srcWidth, uint32_t srcHeight, + uint32_t srcEyeWidth, uint32_t srcEyeHeight); + + // StretchDRS for both eyes (snapshot must already exist in vrRenderSBS). + void StretchDRSBothEyes(ID3D11UnorderedAccessView* dstUAV, uint32_t eyeWidthOut, uint32_t eyeHeightOut, + uint32_t eyeWidthIn, uint32_t eyeHeightIn, uint32_t renderW, uint32_t renderH, + ID3D11ShaderResourceView* srcOverride = nullptr); + + // Hard-copy a DLSS subrect output onto the destination at (offsetX, offsetY). + // No feather/dither — straight CopySubresourceRegion. + void BlendSubrectToOutput(ID3D11Resource* dlssSrc, ID3D11Resource* dst, + uint32_t dstOffsetX, uint32_t dstOffsetY, uint32_t subWidth, uint32_t subHeight, uint32_t srcOffsetX = 0); + + // Hash of per-eye UVs + mode for change detection (forces SL DLSS resource + // recreation). Both eyes are mixed in so asymmetric presets — e.g. Nasal + // Convergence, where rightUV differs from leftUV — don't collide on a + // left-eye-only hash and skip SL recreation. + uint64_t ComputeSubrectUVHash(const Util::Subrect::UVRegion& leftUV, + const Util::Subrect::UVRegion& rightUV, uint32_t mode); +} diff --git a/src/Features/Upscaling/FoveatedRender/Params.cpp b/src/Features/Upscaling/FoveatedRender/Params.cpp new file mode 100644 index 0000000000..ad6eb610c8 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Params.cpp @@ -0,0 +1,70 @@ +#include "Params.h" + +#include "../../../State.h" +#include "../../../Utils/Game.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" +#include "../PerfMode.h" + +namespace FoveatedRenderImpl +{ + VRDlssParams VRDlssParams::Resolve( + ID3D11Resource* upscalingTexture, + ID3D11Resource* depth, + ID3D11Resource* reactive, + ID3D11Resource* transparency, + ID3D11Resource* mvec) + { + VRDlssParams p{}; + + // Dimensions. With DLSSperf (PerfMode) active, the engine RTs (kMAIN, + // depth, mvec) are allocated at RenderRes and state->screenSize is + // spoofed to RenderRes too. PerfMode owns a private DisplayRes + // testTexture that DLSS must target. Mirror Streamline::Upscale's + // plumbing (Streamline.cpp:617-626) so the foveated route works in + // both stacks: input extents read from kMAIN at RenderRes, output + // extents and colorDst point at DisplayRes / testTexture. + auto& perfMode = globals::features::upscaling.perfMode; + const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); + + const auto screenSize = globals::state->screenSize; + const auto renderSize = Util::ConvertToDynamic(screenSize); + const auto displaySize = dlssperfActive ? perfMode.GetDisplayScreenSize() : screenSize; + + p.renderW = (uint32_t)renderSize.x; + p.renderH = (uint32_t)renderSize.y; + p.eyeWidthIn = (uint32_t)(renderSize.x / 2); + p.eyeHeightIn = (uint32_t)renderSize.y; + p.eyeWidthOut = (uint32_t)(displaySize.x / 2); + p.eyeHeightOut = (uint32_t)displaySize.y; + + // Textures. With DLSSperf, DLSS output lands in PerfMode's testTexture + // (DisplayRes); the stretched periphery also targets the testTexture's + // UAV. Without DLSSperf, both alias kMAIN at full size. + p.colorSrc = upscalingTexture; + p.colorDst = dlssperfActive ? static_cast(perfMode.GetTestTexture()) : upscalingTexture; + p.colorDstUAV = dlssperfActive ? perfMode.GetTestTextureUAV() : + globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].UAV; + + p.depthTexture = depth; + p.reactiveMask = reactive; + p.transparencyMask = transparency; + p.motionVectors = mvec; + + // Mode & subrect. PR-1's stereo Subrect API: GetUV() returns the + // primary UV (= left-eye in stereo mode); GetRightEyeUV() returns + // the mirrored right-eye UV. + auto& enhancer = globals::features::upscaling.foveatedRender; + p.mode = enhancer.GetDlssMode(); + p.leftUV = enhancer.subrectController.GetUV(); + p.rightUV = enhancer.subrectController.GetRightEyeUV(); + p.isFullEye = (p.leftUV.w >= 0.999f && p.leftUV.h >= 0.999f); + + // Jitter — ConfigureUpscaling already computed correct DLSS jitter. + auto& upscaling = globals::features::upscaling; + p.jitterX = upscaling.jitter.x; + p.jitterY = upscaling.jitter.y; + + return p; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Params.h b/src/Features/Upscaling/FoveatedRender/Params.h new file mode 100644 index 0000000000..eb85cd70d2 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Params.h @@ -0,0 +1,50 @@ +#pragma once + +#include "../FoveatedRender.h" +#include "Utils/Subrect.h" +#include + +namespace FoveatedRenderImpl +{ + // Unified parameter block consumed by Mode functions. Resolved from + // current global state — when DLSSperf is active, Params::Resolve routes + // `colorDst`/`colorDstUAV` and the output extents through PerfMode's + // testTexture (see Params.cpp). + struct VRDlssParams + { + // Dimensions + uint32_t renderW; // SBS render width (after DRS) + uint32_t renderH; // SBS render height (after DRS) + uint32_t eyeWidthIn; // per-eye input (render) width + uint32_t eyeHeightIn; // per-eye input (render) height + uint32_t eyeWidthOut; // per-eye output (display) width + uint32_t eyeHeightOut; // per-eye output (display) height + + // Textures + ID3D11Resource* colorSrc; // input color (kMAIN) + ID3D11Resource* colorDst; // output color (kMAIN, or PerfMode's testTexture when DLSSperf is active) + ID3D11UnorderedAccessView* colorDstUAV; // UAV for stretch output target + ID3D11Resource* depthTexture; + ID3D11Resource* reactiveMask; + ID3D11Resource* transparencyMask; + ID3D11Resource* motionVectors; + + // Mode & subrect (mode set: kDefault, kFaster). + FoveatedRender::DlssMode mode; + Util::Subrect::UVRegion leftUV; + Util::Subrect::UVRegion rightUV; + bool isFullEye; + + // Jitter (pixel-space, render resolution) + float jitterX; + float jitterY; + + // Build a complete parameter block from current global state. + static VRDlssParams Resolve( + ID3D11Resource* upscalingTexture, + ID3D11Resource* depthTexture, + ID3D11Resource* reactiveMask, + ID3D11Resource* transparencyMask, + ID3D11Resource* motionVectors); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Postprocess.cpp b/src/Features/Upscaling/FoveatedRender/Postprocess.cpp new file mode 100644 index 0000000000..535eea89fa --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Postprocess.cpp @@ -0,0 +1,62 @@ +#include "Postprocess.h" + +#include "../../../Globals.h" +#include "../../../State.h" +#include "../../Upscaling.h" +#include "../FoveatedRender.h" + +#include + +namespace FoveatedRenderImpl +{ + bool Postprocess::ApplyDlssSharpening(Upscaling& upscaling) + { + // sharpnessDLSS <= 0 is the single disable signal — sharpness lives on + // Upscaling::Settings so the route shares the global slider. + const float sharpnessSetting = upscaling.settings.sharpnessDLSS; + if (sharpnessSetting <= 0.0f) { + return true; + } + + if (!upscaling.sharpenerTexture || !upscaling.sharpenerTexture->uav || !upscaling.sharpenerTexture->resource) { + logger::error("[FOVEATED] Missing sharpener resources"); + return false; + } + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + if (!context || !renderer) { + logger::error("[FOVEATED] Missing D3D context or renderer for sharpening"); + return false; + } + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + if (!main.SRV) { + logger::error("[FOVEATED] Missing main SRV for sharpening"); + return false; + } + + // Same exponential mapping Upscaling::ApplySharpening uses: lower + // setting = stronger sharpen. + float currentSharpness = (-2.0f * sharpnessSetting) + 2.0f; + currentSharpness = exp2(-currentSharpness); + + // In-place RCAS on kMAIN through sharpenerTexture. + ID3D11Resource* mainResource = nullptr; + main.SRV->GetResource(&mainResource); + if (!mainResource) { + logger::error("[FOVEATED] Failed to acquire main resource for sharpening"); + return false; + } + + context->OMSetRenderTargets(0, nullptr, nullptr); + upscaling.rcas.ApplySharpen(main.SRV, upscaling.sharpenerTexture->uav.get(), currentSharpness); + context->CopyResource(mainResource, upscaling.sharpenerTexture->resource.get()); + mainResource->Release(); + + if (globals::game::stateUpdateFlags) { + globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); + } + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Postprocess.h b/src/Features/Upscaling/FoveatedRender/Postprocess.h new file mode 100644 index 0000000000..7ccc8dc06b --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Postprocess.h @@ -0,0 +1,16 @@ +#pragma once + +struct Upscaling; + +namespace FoveatedRenderImpl +{ + class Postprocess + { + public: + // Sharpening pass for the FoveatedRender route. Mirrors what + // Upscaling::ApplySharpening does but is invoked from + // Main_PostProcessing only when the FoveatedRender route is active. + // Only the kRCAS path is wired. + static bool ApplyDlssSharpening(Upscaling& upscaling); + }; +} diff --git a/src/Features/Upscaling/FoveatedRender/Preprocess.cpp b/src/Features/Upscaling/FoveatedRender/Preprocess.cpp new file mode 100644 index 0000000000..74cfb4a7a4 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Preprocess.cpp @@ -0,0 +1,109 @@ +#include "Preprocess.h" + +#include "../../../Deferred.h" +#include "../../../State.h" +#include "../../../Util.h" +#include "../../Upscaling.h" + +namespace +{ + ID3D11ComputeShader* GetEnhancerEncodeTexturesCS(Upscaling& upscaling, Upscaling::UpscaleMethod upscaleMethod) + { + uint methodIndex = (uint)upscaleMethod; + if (!upscaling.encodeTexturesCS[methodIndex]) { + std::vector> defines; + defines.push_back({ "DLSS", "" }); + + upscaling.encodeTexturesCS[methodIndex].attach((ID3D11ComputeShader*)Util::CompileShader( + L"Data/Shaders/Upscaling/EncodeTexturesCS.hlsl", defines, "cs_5_0")); + } + + return upscaling.encodeTexturesCS[methodIndex].get(); + } +} + +namespace FoveatedRenderImpl +{ + bool Preprocess::EncodeUpscalingTextures(Upscaling& upscaling) + { + auto upscaleMethod = upscaling.GetUpscaleMethod(); + if (upscaleMethod != Upscaling::UpscaleMethod::kDLSS) { + logger::error("[FOVEATED] Non-DLSS preprocess path is disabled; method={}", (int)upscaleMethod); + return false; + } + + auto state = globals::state; + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + if (!upscaling.upscalingDataCB || !upscaling.reactiveMaskTexture || !upscaling.transparencyCompositionMaskTexture) { + logger::error("[FOVEATED] Missing preprocess resources"); + return false; + } + + // motionVectorCopyTexture is dereferenced unconditionally in the UAV + // array below when method == kDLSS. The above resource check did not + // cover it. Fail closed rather than null-deref. (CodeRabbit on PR #44.) + if (upscaleMethod == Upscaling::UpscaleMethod::kDLSS && !upscaling.motionVectorCopyTexture) { + logger::error("[FOVEATED] Missing motionVectorCopyTexture for DLSS preprocess"); + return false; + } + + auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto& temporalAAMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kTEMPORAL_AA_MASK]; + auto& normals = renderer->GetRuntimeData().renderTargets[globals::deferred->forwardRenderTargets[2]]; + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + // Bail before BeginPerfEvent so the perf-event lifecycle stays balanced. + // CSSetShaderResources with a null view in the array doesn't crash, but + // the encode shader reads all four — a null among them silently corrupts + // the reactive/transparency masks DLSS will sample next. + if (!temporalAAMask.SRV || !normals.SRV || !motionVector.SRV || !depth.depthSRV) { + logger::error("[FOVEATED] Missing preprocess SRV inputs"); + return false; + } + + auto dispatchCount = Util::GetScreenDispatchCount(true); + + state->BeginPerfEvent("FOVEATED Encode Upscaling Textures"); + + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + Upscaling::UpscalingDataCB upscalingData{}; + upscalingData.trueSamplingDim = renderSize; + upscaling.upscalingDataCB->Update(upscalingData); + + auto upscalingBuffer = upscaling.upscalingDataCB->CB(); + context->CSSetConstantBuffers(0, 1, &upscalingBuffer); + + ID3D11ShaderResourceView* views[4] = { temporalAAMask.SRV, normals.SRV, motionVector.SRV, depth.depthSRV }; + context->CSSetShaderResources(0, ARRAYSIZE(views), views); + + ID3D11UnorderedAccessView* uavs[3] = { + upscaling.reactiveMaskTexture->uav.get(), + upscaling.transparencyCompositionMaskTexture->uav.get(), + upscaleMethod == Upscaling::UpscaleMethod::kDLSS ? upscaling.motionVectorCopyTexture->uav.get() : nullptr + }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); + + ID3D11ComputeShader* cs = GetEnhancerEncodeTexturesCS(upscaling, upscaleMethod); + if (!cs) { + state->EndPerfEvent(); + logger::error("[FOVEATED] Failed to get encode compute shader"); + return false; + } + + context->CSSetShader(cs, nullptr, 0); + context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + + ID3D11ShaderResourceView* nullViews[4] = { nullptr, nullptr, nullptr, nullptr }; + context->CSSetShaderResources(0, ARRAYSIZE(nullViews), nullViews); + ID3D11UnorderedAccessView* nullUavs[3] = { nullptr, nullptr, nullptr }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(nullUavs), nullUavs, nullptr); + ID3D11Buffer* nullBuffer = nullptr; + context->CSSetConstantBuffers(0, 1, &nullBuffer); + context->CSSetShader(nullptr, nullptr, 0); + + state->EndPerfEvent(); + return true; + } +} diff --git a/src/Features/Upscaling/FoveatedRender/Preprocess.h b/src/Features/Upscaling/FoveatedRender/Preprocess.h new file mode 100644 index 0000000000..e6bba9b321 --- /dev/null +++ b/src/Features/Upscaling/FoveatedRender/Preprocess.h @@ -0,0 +1,15 @@ +#pragma once + +struct Upscaling; + +namespace FoveatedRenderImpl +{ + class Preprocess + { + public: + // Mirrors Upscaling::EncodeUpscalingTextures (with a DLSS-specific + // shader define) so the FoveatedRender route can prepare reactive + + // transparency masks without touching dev's path. + static bool EncodeUpscalingTextures(Upscaling& upscaling); + }; +} diff --git a/src/Features/Upscaling/PerfMode.cpp b/src/Features/Upscaling/PerfMode.cpp index 611017c498..522773bba0 100644 --- a/src/Features/Upscaling/PerfMode.cpp +++ b/src/Features/Upscaling/PerfMode.cpp @@ -8,8 +8,7 @@ // Quality mode → render-scale resolution is supplied by the FFX SDK helper // (same one Upscaling.cpp uses at ConfigureUpscaling), avoiding a duplicate -// scale table here. Decoupled from the original PR's DlssEnhancer::Bridge so -// PerfMode can ship without the larger enhancer framework. +// scale table here. #include PerfMode::FullscreenPassScope::FullscreenPassScope(ID3D11DeviceContext* a_context) : diff --git a/src/Features/Upscaling/PerfMode.h b/src/Features/Upscaling/PerfMode.h index dee76bc168..0905d61b71 100644 --- a/src/Features/Upscaling/PerfMode.h +++ b/src/Features/Upscaling/PerfMode.h @@ -6,8 +6,7 @@ // // Opt-in VR upscaling feature. Hooks BSOpenVR::GetRenderTargetSize so all // engine render targets are allocated at a small RenderRes while DLSS writes -// its output to a private DisplayRes testTexture. Ships standalone — the -// "DlssEnhancer" prerequisite from earlier drafts no longer applies. +// its output to a private DisplayRes testTexture. Ships standalone. // // Benefits: // - VRAM and bandwidth savings proportional to the quality-mode scale ratio. diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 29e4aebe74..3fc429d2e3 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -10,8 +10,8 @@ #include "../../State.h" #include "../../Util.h" #include "../Upscaling.h" -#include "PerfMode.h" #include "DX12SwapChain.h" +#include "PerfMode.h" namespace { @@ -417,7 +417,7 @@ bool Streamline::IsRTXAndBelow40Series(IDXGIAdapter* a_adapter) return false; } -void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) +void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, uint32_t height) { sl::DLSSOptions dlssOptions{}; @@ -452,7 +452,10 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) const bool dlssperfActive = perfMode.IsHookActive() && perfMode.GetTestTexture(); dlssOptions.outputWidth = width; - dlssOptions.outputHeight = dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y; + // height==0 → caller is the standard upscale path; use full per-eye DisplayRes height. + // Non-zero is the FoveatedRender subrect height — must match extentOut.height or NGX + // produces zeroed output. See SetDLSSOptions decl in Streamline.h for the rationale. + dlssOptions.outputHeight = height != 0 ? height : (dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y); // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 { @@ -515,7 +518,8 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, - const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth) + const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth, + uint32_t outputHeight) { auto context = globals::d3d::context; @@ -552,7 +556,7 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, } }; - SetDLSSOptions(vp, outputWidth); + SetDLSSOptions(vp, outputWidth, outputHeight); sl::ResourceTag tags[] = { { &colorInRes, sl::kBufferTypeScalingInputColor, sl::ResourceLifecycle::eOnlyValidNow, &extentIn }, diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index 2f1f11e866..ea8088f7bf 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -86,11 +86,17 @@ class Streamline ReflexOptionsCache reflexOptionsCache{}; uint32_t lastReflexSleepFrame = UINT32_MAX; - // Helper: Execute DLSS for a single viewport with given resources + // Helper: Execute DLSS for a single viewport with given resources. + // outputHeight defaults to 0 → SetDLSSOptions uses full per-eye DisplayRes height + // (matches the standard upscale path where every eval is full eye). FoveatedRender's + // subrect path must pass the actual subrect height so DLSS isn't configured for + // `subOutW × eyeHeightOut` while extentOut says `subOutW × subOutH` — that mismatch + // makes NGX return zeroed output and the subrect region renders black. void EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, - const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth); + const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth, + uint32_t outputHeight = 0); // Cached DLL version info for Streamline plugin directory static std::vector> dllVersions; @@ -106,7 +112,9 @@ class Streamline bool IsRTXAndBelow40Series(IDXGIAdapter* a_adapter); - void SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width); + // height = 0 → use full per-eye DisplayRes height (default for the standard + // upscale path). Non-zero is the subrect height the FoveatedRender route needs. + void SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, uint32_t height = 0); void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors); void UpdateReflex(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 497945ae16..35963c05f5 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -14,6 +14,7 @@ #include "Features/InteriorSun.h" #include "Features/LightLimitFix.h" #include "Features/Upscaling.h" +#include "Features/Upscaling/FoveatedRender/Bridge.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" @@ -411,6 +412,13 @@ struct BSShaderRenderTargets_Create // upscaling toggle), so SetupResources runs here directly. if (perfMode.IsHookActive()) perfMode.SetupResources(); + + // PR-3 MVP-B: latch FoveatedRender enable + qualityMode at the moment + // the engine is fully initialized but before the first frame. After + // this point, live setting changes won't be honored mid-game (matches + // Streamline's DLSS option lifecycle — quality changes need a full + // resource recreate the user has to opt into). + FoveatedRenderImpl::Bridge::BootSequence(); } static inline REL::Relocation func; }; diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 0284d42cb6..84d0eb61bb 100644 --- a/src/Utils/Subrect.cpp +++ b/src/Utils/Subrect.cpp @@ -5,6 +5,10 @@ #include #include +// OpaquePreviewBlendCallback lives in Subrect_PreviewBlend.cpp — that TU +// reaches into the plugin's d3d singletons, which the unit-test target +// (tests/cpp pulls Subrect.cpp standalone) can't link against. + namespace { Util::Subrect::UVRegion ClampUV(Util::Subrect::UVRegion uv) diff --git a/src/Utils/Subrect.h b/src/Utils/Subrect.h index c5c0a4705b..c5c600f257 100644 --- a/src/Utils/Subrect.h +++ b/src/Utils/Subrect.h @@ -82,7 +82,9 @@ namespace Util::Subrect // texture; crop UV stays in [0,1] of that window. imageRenderCallback, // when non-null, is queued via ImDrawList::AddCallback around the // preview Image draw (paired with ImDrawCallback_ResetRenderState) so - // hosts can override blend state for the image specifically. + // hosts can override blend state for the image specifically. Pass + // OpaquePreviewBlendCallback when the preview texture is an RT with + // non-1 alpha (kMAIN, etc.) to suppress menu-background bleed-through. void DrawEditor(ID3D11ShaderResourceView* previewSrv, ID3D11Texture2D* previewTexture, float uvVisibleWidth = 1.0f, float uvStartX = 0.0f, ImDrawCallback imageRenderCallback = nullptr); @@ -125,4 +127,21 @@ namespace Util::Subrect void ApplyPreset(int index); void SyncRightUV(); }; + + // Opaque-RGB blend state callback for Controller::DrawEditor. Pass when the + // preview SRV is a render target with non-1 alpha (kMAIN, kTOTAL, etc.). + // ImGui's default SRC_ALPHA blend would let the menu background bleed + // through where the source alpha is < 1, making the preview look like a + // transparency mask. This callback switches to opaque RGB-only writes + // around the Image draw; DrawEditor queues ImDrawCallback_ResetRenderState + // immediately after to restore default state. + // + // Two non-obvious regression risks if reimplemented: + // 1. BlendEnable must stay FALSE — SRC_ALPHA causes the bleed-through. + // 2. WriteMask must exclude alpha (RGB only). In VR, Skyrim's menu UI + // shader recomposites the menu plate over the SBS framebuffer with + // alpha blending; writing texture alpha into the menu plate RT + // produces a cutout visible only through the HMD. RGB-only writes + // leave the plate's pre-cleared alpha=1 in place. + void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*); } // namespace Util::Subrect diff --git a/src/Utils/Subrect_PreviewBlend.cpp b/src/Utils/Subrect_PreviewBlend.cpp new file mode 100644 index 0000000000..287eeed0cb --- /dev/null +++ b/src/Utils/Subrect_PreviewBlend.cpp @@ -0,0 +1,42 @@ +// Separate TU for Util::Subrect::OpaquePreviewBlendCallback. Split from +// Subrect.cpp because this needs the plugin's d3d singletons (globals::d3d), +// and Subrect.cpp is also compiled standalone by the unit-test target +// (tests/cpp/CMakeLists.txt) which has no PCH and no D3D context to bind. +// Plugin builds pick this up automatically via the src/*.cpp GLOB_RECURSE. + +#include "Globals.h" +#include "Utils/D3D.h" +#include "Utils/Subrect.h" + +#include +#include +#include + +namespace Util::Subrect +{ + void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) + { + auto* device = globals::d3d::device; + auto* context = globals::d3d::context; + if (!device || !context) { + return; + } + + static winrt::com_ptr opaqueBlend; + if (!opaqueBlend) { + D3D11_BLEND_DESC desc{}; + desc.RenderTarget[0].BlendEnable = FALSE; + desc.RenderTarget[0].RenderTargetWriteMask = + D3D11_COLOR_WRITE_ENABLE_RED | + D3D11_COLOR_WRITE_ENABLE_GREEN | + D3D11_COLOR_WRITE_ENABLE_BLUE; + if (FAILED(device->CreateBlendState(&desc, opaqueBlend.put()))) { + return; + } + Util::SetResourceName(opaqueBlend.get(), "Subrect::OpaquePreviewBlend"); + } + if (opaqueBlend) { + context->OMSetBlendState(opaqueBlend.get(), nullptr, 0xFFFFFFFF); + } + } +} // namespace Util::Subrect