From 5365d46e0d3f4211c92753b5ed1def1a59223b0e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 5 Feb 2026 00:14:24 -0800 Subject: [PATCH 1/2] fix(VR): apply per eye upscaling --- .../Shaders/Upscaling/ClearHMDMaskCS.hlsl | 24 ++ src/Features/Upscaling.cpp | 304 +++++++++++++++++- src/Features/Upscaling.h | 30 ++ src/Features/Upscaling/FidelityFX.cpp | 138 +++++--- src/Features/Upscaling/FidelityFX.h | 2 +- src/Features/Upscaling/Streamline.cpp | 252 +++++++++++---- src/Features/Upscaling/Streamline.h | 13 +- src/Utils/UI.h | 19 +- 8 files changed, 659 insertions(+), 123 deletions(-) create mode 100644 features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl diff --git a/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl b/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl new file mode 100644 index 0000000000..bbc2f45575 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl @@ -0,0 +1,24 @@ +// Zeros color in the HMD hidden area per eye. +// Prevents DLSS/FSR from temporally accumulating the engine's sky/ambient clear color +// into visible pixels during head movement ("light blue border" ghosting). +// depth == 0.0 is the unrendered/hidden area value (Skyrim reversed-Z: far plane = 0). +// DepthIn is the combined stereo depth buffer; DepthOffsetX selects the eye's half. +// ColorInOut is the isolated per-eye buffer; ColorOffsetX is always 0. + +cbuffer ClearHMDMaskCB : register(b0) +{ + uint DepthOffsetX; // X offset into combined stereo depth (0 = left, eyeWidth = right) + uint ColorOffsetX; // X offset into color target (always 0 for per-eye buffers) + uint pad0; + uint pad1; +}; + +Texture2D DepthIn : register(t0); +RWTexture2D ColorInOut : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) +{ + // Read from stereo depth, write to potentially stereo color + if (DepthIn[dispatchID.xy + uint2(DepthOffsetX, 0)] == 0.0) + ColorInOut[dispatchID.xy + uint2(ColorOffsetX, 0)] = float4(0.0, 0.0, 0.0, 0.0); +} diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index faae04e175..8bf23a836b 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -6,6 +6,7 @@ #include "Upscaling/DX12SwapChain.h" #include "Upscaling/FidelityFX.h" #include "Upscaling/Streamline.h" +#include "Utils/UI.h" #include "VR.h" #include #include @@ -182,12 +183,13 @@ void Upscaling::DrawSettings() // Check the current upscale method auto upscaleMethod = GetUpscaleMethod(); - // Display warning for DLSS resolution limits - if (upscaleMethod == UpscaleMethod::kDLSS) { + // Display warning for DLSS resolution limits (non-VR only; VR handles this automatically) + if (!globals::game::isVR && upscaleMethod == UpscaleMethod::kDLSS) { auto screenSize = globals::state->screenSize; if (screenSize.x > streamline.MAX_RESOLUTION || screenSize.y > streamline.MAX_RESOLUTION) { ImGui::PushStyleColor(ImGuiCol_Text, Util::Colors::GetWarning()); - ImGui::Text("Warning: Requested resolution %.0f x %.0f exceeds maximum supported resolution %d x %d for DLSS.", screenSize.x, screenSize.y, streamline.MAX_RESOLUTION, streamline.MAX_RESOLUTION); + ImGui::Text("Warning: Requested resolution %.0f x %.0f exceeds maximum supported resolution %d x %d for DLSS.", + screenSize.x, screenSize.y, streamline.MAX_RESOLUTION, streamline.MAX_RESOLUTION); ImGui::Text("DLSS will not function. Lower your resolution or select a different upscaling method."); ImGui::PopStyleColor(); } @@ -312,6 +314,61 @@ void Upscaling::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G."); } + + // VR Debug visualization -- per-eye buffers and native inputs + if (globals::game::isVR) { + ImGui::Separator(); + static float debugRescale = 0.15f; + ImGui::SliderFloat("View Resize", &debugRescale, 0.05f, 1.f); + + if (ImGui::TreeNode("Upscaling Intermediates")) { + if (vrIntermediateColorIn[0] && vrIntermediateColorOut[0]) { + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[0], "Left Eye In", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[1], "Right Eye In", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[0], "Left Eye Out", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[1], "Right Eye Out", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[0], "Left Eye MVec", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[1], "Right Eye MVec", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[0], "Left Eye Reactive", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[1], "Right Eye Reactive", debugRescale) + } else { + ImGui::TextDisabled("VR intermediates not yet created (enter game world)"); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("Native Inputs")) { + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto& mvec = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + auto DisplayRT = [&](const char* label, ID3D11Texture2D* tex, ID3D11ShaderResourceView* srv) { + if (srv && tex) { + D3D11_TEXTURE2D_DESC desc; + tex->GetDesc(&desc); + char buf[128]; + snprintf(buf, sizeof(buf), "%s (%ux%u)", label, desc.Width, desc.Height); + if (ImGui::TreeNode(buf)) { + ImGui::Image(srv, { desc.Width * debugRescale, desc.Height * debugRescale }); + ImGui::TreePop(); + } + } + }; + + DisplayRT("kMAIN (Color Input)", (ID3D11Texture2D*)main.texture, (ID3D11ShaderResourceView*)main.SRV); + DisplayRT("Motion Vectors", (ID3D11Texture2D*)mvec.texture, (ID3D11ShaderResourceView*)mvec.SRV); + DisplayRT("Depth", depth.texture, depth.depthSRV); + + if (reactiveMaskTexture) + BUFFER_VIEWER_NODE_TITLE(reactiveMaskTexture, "Reactive Mask", debugRescale) + if (transparencyCompositionMaskTexture) + BUFFER_VIEWER_NODE_TITLE(transparencyCompositionMaskTexture, "Transparency Mask", debugRescale) + + ImGui::TreePop(); + } + } + ImGui::Separator(); // FidelityFX section if (ImGui::Selectable("AMD FidelityFX DLLs (click to open folder)")) { @@ -598,6 +655,18 @@ void Upscaling::CheckResources(UpscaleMethod a_upscalemethod) streamline.DestroyDLSSResources(); else if (previousUpscaleMode == UpscaleMethod::kFSR) fidelityFX.DestroyFSRResources(); + + if (globals::game::isVR) { + 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(); + } + vrResourcesAllocated[0] = vrResourcesAllocated[1] = false; + } } if (a_upscalemethod == UpscaleMethod::kFSR) fidelityFX.CreateFSRResources(); @@ -675,6 +744,229 @@ ID3D11VertexShader* Upscaling::GetUpscaleVS() return upscaleVS.get(); } +eastl::unique_ptr Upscaling::CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, + bool copyBindFlags, bool createSRV, bool createUAV, const char* name) +{ + D3D11_TEXTURE2D_DESC srcDesc; + static_cast(src)->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 Upscaling::CreateVRIntermediateTextures(uint32_t inWidth, uint32_t inHeight, uint32_t outWidth, uint32_t outHeight, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc) +{ + // All buffers are per-eye: Streamline validates all extents against the input color texture + // dimensions, so every tagged resource must be isolated per-eye at {0,0}. + for (int i = 0; i < 2; i++) { + std::string suffix = (i == 0) ? "Left" : "Right"; + + vrIntermediateColorIn[i] = CreateTextureFromSource(colorSrc, inWidth, inHeight, false, true, true, ("Upscale_ColorIn_" + suffix).c_str()); + vrIntermediateColorOut[i] = CreateTextureFromSource(colorSrc, outWidth, outHeight, false, true, false, ("Upscale_ColorOut_" + suffix).c_str()); + + // Depth: R32_TYPELESS base (matches kMAIN), with R32_FLOAT SRV for ClearHMDMaskCS. + // CopySubresourceRegion requires matching typeless formats; SRV reinterprets as R32_FLOAT. + { + 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; + vrIntermediateDepth[i] = eastl::make_unique(depthDesc); + + Util::SetResourceName(vrIntermediateDepth[i]->resource.get(), ("Upscale_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; + vrIntermediateDepth[i]->CreateSRV(srvDesc); + } + + vrIntermediateMotionVectors[i] = CreateTextureFromSource(mvecSrc, inWidth, inHeight, false, true, false, ("Upscale_MVec_" + suffix).c_str()); + vrIntermediateReactiveMask[i] = CreateTextureFromSource(reactiveSrc, inWidth, inHeight, false, true, false, ("Upscale_Reactive_" + suffix).c_str()); + vrIntermediateTransparencyMask[i] = CreateTextureFromSource(transparencySrc, inWidth, inHeight, false, true, false, ("Upscale_Transparency_" + suffix).c_str()); + } + + logger::info("[Upscaling] Created VR intermediate textures: per-eye in {}x{}, out {}x{}", + inWidth, inHeight, outWidth, outHeight); +} + +void Upscaling::PreparePerEyeInputs(ID3D11Resource* colorSrc, ID3D11Resource* depthSrc, ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc) +{ + if (!globals::game::isVR) + return; + + auto state = globals::state; + if (state->frameAnnotations) + state->BeginPerfEvent("VR Upscaling Prepare"); + + auto context = globals::d3d::context; + auto screenSize = state->screenSize; + auto renderSize = Util::ConvertToDynamic(screenSize); + + uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); + uint32_t eyeHeightOut = (uint32_t)screenSize.y; + uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); + uint32_t eyeHeightIn = (uint32_t)renderSize.y; + + bool needsRecreate = !vrIntermediateColorIn[0] || !vrIntermediateColorOut[0]; + if (!needsRecreate) { + needsRecreate = (vrIntermediateColorIn[0]->desc.Width != eyeWidthIn || + vrIntermediateColorIn[0]->desc.Height != eyeHeightIn); + } + if (needsRecreate) { + logger::info("[Upscaling] (Re)creating VR intermediates: per-eye in {}x{}, out {}x{}", + eyeWidthIn, eyeHeightIn, eyeWidthOut, eyeHeightOut); + CreateVRIntermediateTextures(eyeWidthIn, eyeHeightIn, eyeWidthOut, eyeHeightOut, + colorSrc, mvecSrc, reactiveSrc, transparencySrc); + vrResourcesAllocated[0] = vrResourcesAllocated[1] = false; + } + + // Extract both eyes' inputs from combined stereo buffers + 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(vrIntermediateColorIn[i]->resource.get(), 0, 0, 0, 0, colorSrc, 0, &srcBox); + context->CopySubresourceRegion(vrIntermediateDepth[i]->resource.get(), 0, 0, 0, 0, depthSrc, 0, &srcBox); + context->CopySubresourceRegion(vrIntermediateMotionVectors[i]->resource.get(), 0, 0, 0, 0, mvecSrc, 0, &srcBox); + context->CopySubresourceRegion(vrIntermediateTransparencyMask[i]->resource.get(), 0, 0, 0, 0, transparencySrc, 0, &srcBox); + context->CopySubresourceRegion(vrIntermediateReactiveMask[i]->resource.get(), 0, 0, 0, 0, reactiveSrc, 0, &srcBox); + } + + // Zero color where depth == 0 (HMD hidden area) in each per-eye buffer. + // Depth is read from the combined stereo SRV at the per-eye offset; color is written + // to the isolated per-eye UAV (ColorOffsetX = 0). + auto& depthTexture = globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + for (uint32_t i = 0; i < 2; ++i) { + uint32_t depthOffset = (i == 1) ? eyeWidthIn : 0; + ClearHMDMask(vrIntermediateColorIn[i]->uav.get(), depthTexture.depthSRV, + eyeWidthIn, eyeHeightIn, depthOffset, 0); + } + + if (state->frameAnnotations) + state->EndPerfEvent(); +} + +void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) +{ + if (!globals::game::isVR) + return; + + auto state = globals::state; + if (state->frameAnnotations) + state->BeginPerfEvent("VR Upscaling Finalize"); + + auto context = globals::d3d::context; + auto screenSize = state->screenSize; + + uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); + uint32_t eyeHeightOut = (uint32_t)screenSize.y; + + // Write upscaled outputs back + 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, vrIntermediateColorOut[i]->resource.get(), 0, &outBox); + } + + if (state->frameAnnotations) + state->EndPerfEvent(); +} + +void Upscaling::ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, + uint32_t eyeWidth, uint32_t eyeHeight, uint32_t depthOffsetX, uint32_t colorOffsetX) +{ + if (!globals::game::isVR) + return; + + auto context = globals::d3d::context; + + if (!vrClearHMDMaskCS) { + vrClearHMDMaskCS.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/ClearHMDMaskCS.hlsl", {}, "cs_5_0")); + + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.ByteWidth = 16; // 4 uints + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + DX::ThrowIfFailed(globals::d3d::device->CreateBuffer(&cbDesc, nullptr, vrClearHMDMaskCB.put())); + } + + if (vrClearHMDMaskCS) { + auto dispatchX = (eyeWidth + 7) / 8; + auto dispatchY = (eyeHeight + 7) / 8; + + context->CSSetShader(vrClearHMDMaskCS.get(), nullptr, 0); + + ID3D11ShaderResourceView* srvs[1] = { depthSRV }; + context->CSSetShaderResources(0, 1, srvs); + + ID3D11UnorderedAccessView* uavs[1] = { colorUAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + + D3D11_MAPPED_SUBRESOURCE mapped{}; + context->Map(vrClearHMDMaskCB.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + + uint32_t offsets[4] = { depthOffsetX, colorOffsetX, 0, 0 }; + + memcpy(mapped.pData, offsets, sizeof(offsets)); + context->Unmap(vrClearHMDMaskCB.get(), 0); + + ID3D11Buffer* cbs[1] = { vrClearHMDMaskCB.get() }; + context->CSSetConstantBuffers(0, 1, cbs); + + context->Dispatch(dispatchX, dispatchY, 1); + + // Unbind + ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; + ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; + ID3D11Buffer* nullCB[1] = { nullptr }; + context->CSSetShaderResources(0, 1, nullSRV); + context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); + context->CSSetConstantBuffers(0, 1, nullCB); + context->CSSetShader(nullptr, nullptr, 0); + } +} + int32_t GetJitterPhaseCount(int32_t renderWidth, int32_t displayWidth) { const float basePhaseCount = 8.0f; @@ -1139,7 +1431,11 @@ void Upscaling::LoadUpscalingSDKs() void Upscaling::CheckFrameConstants() { - streamline.CheckFrameConstants(); + // In VR, constants are set per-eye in the Upscale() loop + // Skip the early call from DeferredPasses to avoid issues + if (globals::game::isVR) + return; + streamline.CheckFrameConstants(streamline.viewport, 0); } void Upscaling::SetUIBuffer() diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index cdeeda0818..790f00b239 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -130,6 +130,36 @@ struct Upscaling : Feature winrt::com_ptr upscaleBlendState; winrt::com_ptr upscaleRasterizerState; + // Shared VR HMD Mask Clearing + winrt::com_ptr vrClearHMDMaskCS; + winrt::com_ptr vrClearHMDMaskCB; + // Helper to dispatch mask clearing for a single eye region + void ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, + uint32_t eyeWidth, uint32_t eyeHeight, uint32_t depthOffsetX, uint32_t colorOffsetX); + + // Shared VR Per-Eye Intermediate Buffers + // Owned here so both Streamline (DLSS) and FidelityFX (FSR) can use them. + eastl::unique_ptr vrIntermediateColorIn[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateColorOut[2]; // per-eye output resolution + eastl::unique_ptr vrIntermediateDepth[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateMotionVectors[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateReactiveMask[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateTransparencyMask[2]; // per-eye render resolution + bool vrResourcesAllocated[2] = { false, false }; + + // Helper to create/resize per-eye buffers matching source formats + void CreateVRIntermediateTextures(uint32_t inWidth, uint32_t inHeight, uint32_t outWidth, uint32_t outHeight, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + + // Helper: Create a Texture2D matching source format at a given size + static 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); + + // Shared Pipeline Steps + void PreparePerEyeInputs(ID3D11Resource* colorSrc, ID3D11Resource* depthSrc, ID3D11Resource* mvecSrc, + ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + void FinalizePerEyeOutputs(ID3D11Resource* colorDst); + void ConfigureTAA(); void ConfigureUpscaling(RE::BSGraphics::State* a_state); void Upscale(); diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index c5c0bec9ad..c471c25187 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -194,7 +194,8 @@ void FidelityFX::CreateFSRResources() auto fsrDevice = ffxGetDeviceDX11(globals::d3d::device); - size_t scratchBufferSize = ffxGetScratchMemorySizeDX11(FFX_FSR3UPSCALER_CONTEXT_COUNT); + uint32_t numContexts = globals::game::isVR ? 2 : 1; + size_t scratchBufferSize = ffxGetScratchMemorySizeDX11(numContexts); fsrScratchBuffer = calloc(scratchBufferSize, 1); if (!fsrScratchBuffer) { logger::critical("[FidelityFX] Failed to allocate FSR3 scratch buffer memory!"); @@ -203,35 +204,50 @@ void FidelityFX::CreateFSRResources() memset(fsrScratchBuffer, 0, scratchBufferSize); FfxInterface fsrInterface; - if (ffxGetInterfaceDX11(&fsrInterface, fsrDevice, fsrScratchBuffer, scratchBufferSize, FFX_FSR3UPSCALER_CONTEXT_COUNT) != FFX_OK) { + if (ffxGetInterfaceDX11(&fsrInterface, fsrDevice, fsrScratchBuffer, scratchBufferSize, numContexts) != FFX_OK) { logger::critical("[FidelityFX] Failed to initialize FSR3 backend interface!"); free(fsrScratchBuffer); fsrScratchBuffer = nullptr; return; } - FfxFsr3ContextDescription contextDescription; - contextDescription.maxRenderSize.width = (uint)state->screenSize.x; - contextDescription.maxRenderSize.height = (uint)state->screenSize.y; - contextDescription.maxUpscaleSize.width = (uint)state->screenSize.x; - contextDescription.maxUpscaleSize.height = (uint)state->screenSize.y; - contextDescription.displaySize.width = (uint)state->screenSize.x; - contextDescription.displaySize.height = (uint)state->screenSize.y; - contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE | FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; - contextDescription.backendInterfaceUpscaling = fsrInterface; - - if (ffxFsr3ContextCreate(&fsrContext, &contextDescription) != FFX_OK) { - logger::critical("[FidelityFX] Failed to initialize FSR3 context!"); - free(fsrScratchBuffer); - fsrScratchBuffer = nullptr; - return; + auto screenSize = state->screenSize; + auto renderSize = Util::ConvertToDynamic(screenSize); + + uint32_t displayWidth = (uint32_t)(globals::game::isVR ? screenSize.x / 2 : screenSize.x); + uint32_t displayHeight = (uint32_t)screenSize.y; + uint32_t renderWidth = (uint32_t)(globals::game::isVR ? renderSize.x / 2 : renderSize.x); + uint32_t renderHeight = (uint32_t)renderSize.y; + + for (uint32_t i = 0; i < numContexts; ++i) { + FfxFsr3ContextDescription contextDescription; + contextDescription.maxRenderSize.width = renderWidth; + contextDescription.maxRenderSize.height = renderHeight; + contextDescription.maxUpscaleSize.width = displayWidth; + contextDescription.maxUpscaleSize.height = displayHeight; + contextDescription.displaySize.width = displayWidth; + contextDescription.displaySize.height = displayHeight; + contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE | FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; + contextDescription.backendInterfaceUpscaling = fsrInterface; + + if (ffxFsr3ContextCreate(&fsrContext[i], &contextDescription) != FFX_OK) { + logger::critical("[FidelityFX] Failed to initialize FSR3 context for eye {}!", i); + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + return; + } } + logger::info("[FidelityFX] Created {} FSR3 contexts (Display: {}x{}, Render: {}x{})", + numContexts, displayWidth, displayHeight, renderWidth, renderHeight); } void FidelityFX::DestroyFSRResources() { - if (ffxFsr3ContextDestroy(&fsrContext) != FFX_OK) - logger::critical("[FidelityFX] Failed to destroy FSR3 context!"); + uint32_t numContexts = globals::game::isVR ? 2 : 1; + for (uint32_t i = 0; i < numContexts; ++i) { + if (ffxFsr3ContextDestroy(&fsrContext[i]) != FFX_OK) + logger::critical("[FidelityFX] Failed to destroy FSR3 context for eye {}!", i); + } // Free the scratch buffer to prevent memory leak if (fsrScratchBuffer) { @@ -271,54 +287,94 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); - { - FfxFsr3DispatchUpscaleDescription dispatchParameters{}; + auto& upscaling = globals::features::upscaling; + auto jitter = upscaling.jitter; + + auto DispatchFSR = [&](uint32_t contextIndex, ID3D11Resource* r_color, ID3D11Resource* r_depth, ID3D11Resource* r_mvec, + ID3D11Resource* r_reactive, ID3D11Resource* r_trans, ID3D11Resource* r_output, + uint32_t r_width, float mv_scale_x) { + if (state->frameAnnotations) { + if (globals::game::isVR) { + char buf[32]; + snprintf(buf, sizeof(buf), "FSR Dispatch Eye %u", contextIndex); + state->BeginPerfEvent(buf); + } else { + state->BeginPerfEvent("FSR Dispatch"); + } + } + FfxFsr3DispatchUpscaleDescription dispatchParameters{}; dispatchParameters.commandList = ffxGetCommandListDX11(context); - dispatchParameters.color = ffxGetResource(a_upscalingTexture, L"FSR3_Input_OutputColor"); - dispatchParameters.depth = ffxGetResource(depthTexture.texture, L"FSR3_InputDepth"); - dispatchParameters.motionVectors = ffxGetResource(a_motionVectors, L"FSR3_InputMotionVectors"); + dispatchParameters.color = ffxGetResource(r_color, L"FSR3_Input_OutputColor"); + dispatchParameters.depth = ffxGetResource(r_depth, L"FSR3_InputDepth"); + dispatchParameters.motionVectors = ffxGetResource(r_mvec, L"FSR3_InputMotionVectors"); dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); - dispatchParameters.upscaleOutput = dispatchParameters.color; - dispatchParameters.reactive = ffxGetResource(a_reactiveMask, L"FSR3_InputReactiveMap"); - dispatchParameters.transparencyAndComposition = ffxGetResource(a_transparencyCompositionMask, L"FSR3_TransparencyAndCompositionMap"); + dispatchParameters.upscaleOutput = ffxGetResource(r_output, L"FSR3_OutputColor"); + dispatchParameters.reactive = ffxGetResource(r_reactive, L"FSR3_InputReactiveMap"); + dispatchParameters.transparencyAndComposition = ffxGetResource(r_trans, L"FSR3_TransparencyAndCompositionMap"); - dispatchParameters.motionVectorScale.x = globals::game::isVR ? renderSize.x * 0.5f : renderSize.x; + dispatchParameters.motionVectorScale.x = mv_scale_x; dispatchParameters.motionVectorScale.y = renderSize.y; - dispatchParameters.renderSize.width = (uint)renderSize.x; + dispatchParameters.renderSize.width = r_width; dispatchParameters.renderSize.height = (uint)renderSize.y; - auto& upscaling = globals::features::upscaling; - auto jitter = upscaling.jitter; - dispatchParameters.jitterOffset.x = -jitter.x; dispatchParameters.jitterOffset.y = -jitter.y; dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; - dispatchParameters.cameraFar = *globals::game::cameraFar; dispatchParameters.cameraNear = *globals::game::cameraNear; - dispatchParameters.enableSharpening = true; dispatchParameters.sharpness = a_sharpness; - dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; dispatchParameters.reset = false; dispatchParameters.preExposure = 1.0f; - dispatchParameters.flags = 0; - // Wrap FSR dispatch in SEH to catch crashes when RenderDoc is active __try { - if (ffxFsr3ContextDispatchUpscale(&fsrContext, &dispatchParameters) != FFX_OK) - logger::critical("[FidelityFX] Failed to dispatch upscaling!"); + if (ffxFsr3ContextDispatchUpscale(&fsrContext[contextIndex], &dispatchParameters) != FFX_OK) + logger::critical("[FidelityFX] Failed to dispatch upscaling for eye {}!", contextIndex); } __except (EXCEPTION_EXECUTE_HANDLER) { if (!fsrDispatchCrashLogged) { - logger::critical("[FidelityFX] FSR3 dispatch crashed - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture."); + logger::critical("[FidelityFX] FSR3 dispatch crashed for eye {} - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture.", contextIndex); fsrDispatchCrashLogged = true; } - // Continue execution instead of crashing } + + if (state->frameAnnotations) + state->EndPerfEvent(); + }; + + if (globals::game::isVR) { + // Prepare per-eye inputs and clear mask + upscaling.PreparePerEyeInputs(a_upscalingTexture, depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask); + + uint32_t numViews = 2; + uint32_t eyeWidth = (uint32_t)(renderSize.x / 2); + for (uint32_t i = 0; i < numViews; ++i) { + DispatchFSR(i, + upscaling.vrIntermediateColorIn[i]->resource.get(), + upscaling.vrIntermediateDepth[i]->resource.get(), + upscaling.vrIntermediateMotionVectors[i]->resource.get(), + upscaling.vrIntermediateReactiveMask[i]->resource.get(), + upscaling.vrIntermediateTransparencyMask[i]->resource.get(), + upscaling.vrIntermediateColorOut[i]->resource.get(), + eyeWidth, + renderSize.x / 2.0f); + } + + // Merge outputs back to kMAIN + upscaling.FinalizePerEyeOutputs(a_upscalingTexture); + } else { + DispatchFSR(0, + a_upscalingTexture, + depthTexture.texture, + a_motionVectors, + a_reactiveMask, + a_transparencyCompositionMask, + a_upscalingTexture, // Output to same texture + (uint)renderSize.x, + renderSize.x); } } \ No newline at end of file diff --git a/src/Features/Upscaling/FidelityFX.h b/src/Features/Upscaling/FidelityFX.h index 6fe581c172..c44953baad 100644 --- a/src/Features/Upscaling/FidelityFX.h +++ b/src/Features/Upscaling/FidelityFX.h @@ -26,7 +26,7 @@ class FidelityFX ffx::Context swapChainContext{}; ffx::Context frameGenContext; - FfxFsr3Context fsrContext; + FfxFsr3Context fsrContext[2]; bool featureFSR3FG = false; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 0ee999db49..cecb137532 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -200,58 +200,79 @@ void Streamline::PostDevice() * * Populates and submits camera parameters, projection matrices, motion vector settings, and other per-frame constants to the Streamline SDK for the current frame. Uses cached framebuffer data and global state to ensure correct configuration for upscaling and frame generation features. */ -void Streamline::CheckFrameConstants() +void Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex) { - if (frameChecker.IsNewFrame() && globals::features::upscaling.streamline.initialized) { + if (!globals::features::upscaling.streamline.initialized) + return; + + // Get new frame token once per frame (only on first call) + if (frameChecker.IsNewFrame()) { slGetNewFrameToken(frameToken, &globals::state->frameCount); + } - auto state = globals::state; + // In VR, we need to set constants for each viewport/eye separately + // In non-VR, this is called once per frame + auto state = globals::state; - sl::Constants slConstants = {}; + sl::Constants slConstants = {}; - if (globals::game::isVR) { - slConstants.cameraAspectRatio = (state->screenSize.x * 0.5f) / state->screenSize.y; - } else { - slConstants.cameraAspectRatio = state->screenSize.x / state->screenSize.y; - } + // Calculate aspect ratio for the SINGLE EYE + float eyeWidth = state->screenSize.x * (globals::game::isVR ? 0.5f : 1.0f); + slConstants.cameraAspectRatio = eyeWidth / state->screenSize.y; + + slConstants.cameraFOV = Util::GetVerticalFOVRad(); + slConstants.cameraNear = *globals::game::cameraNear; + slConstants.cameraFar = *globals::game::cameraFar; + + auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse(eyeIndex).Transpose(); + auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered(eyeIndex).Transpose(); - slConstants.cameraFOV = Util::GetVerticalFOVRad(); - slConstants.cameraNear = *globals::game::cameraNear; - slConstants.cameraFar = *globals::game::cameraFar; + slConstants.cameraMotionIncluded = sl::Boolean::eTrue; + slConstants.cameraPinholeOffset = { 0.f, 0.f }; + slConstants.cameraRight = { viewMatrix._11, viewMatrix._12, viewMatrix._13 }; + slConstants.cameraUp = { viewMatrix._21, viewMatrix._22, viewMatrix._23 }; + slConstants.cameraFwd = { viewMatrix._31, viewMatrix._32, viewMatrix._33 }; + slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(eyeIndex); + slConstants.cameraViewToClip = *(sl::float4x4*)&cameraViewToClip; + slConstants.depthInverted = sl::Boolean::eFalse; - auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse().Transpose(); - auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered().Transpose(); + if (globals::game::isVR) { + // VR: compute clipToCameraView / clipToPrevClip / prevClipToClip from Skyrim's per-eye matrices. + // recalculateCameraMatrices() uses a single static prev-frame slot -- unusable for two viewports. + sl::matrixFullInvert(slConstants.clipToCameraView, slConstants.cameraViewToClip); - slConstants.cameraMotionIncluded = sl::Boolean::eTrue; - slConstants.cameraPinholeOffset = { 0.f, 0.f }; - slConstants.cameraRight = { viewMatrix._11, viewMatrix._12, viewMatrix._13 }; - slConstants.cameraUp = { viewMatrix._21, viewMatrix._22, viewMatrix._23 }; - slConstants.cameraFwd = { viewMatrix._31, viewMatrix._32, viewMatrix._33 }; - slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(); - slConstants.cameraViewToClip = *(sl::float4x4*)&cameraViewToClip; - slConstants.depthInverted = sl::Boolean::eFalse; + auto currViewProj = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Transpose(); + auto prevViewProj = globals::game::frameBufferCached.GetCameraPreviousViewProjUnjittered(eyeIndex).Transpose(); + sl::float4x4 currViewProjSL = *(sl::float4x4*)&currViewProj; + sl::float4x4 prevViewProjSL = *(sl::float4x4*)&prevViewProj; + + sl::float4x4 invCurrViewProj; + sl::matrixFullInvert(invCurrViewProj, currViewProjSL); + sl::matrixMul(slConstants.clipToPrevClip, invCurrViewProj, prevViewProjSL); + sl::matrixFullInvert(slConstants.prevClipToClip, slConstants.clipToPrevClip); + } else { recalculateCameraMatrices(slConstants); + } - auto& upscaling = globals::features::upscaling; - auto jitter = upscaling.jitter; - slConstants.jitterOffset = { -jitter.x, -jitter.y }; - slConstants.reset = sl::Boolean::eFalse; - - slConstants.mvecScale = { (globals::game::isVR ? 0.5f : 1.0f), 1 }; - slConstants.motionVectors3D = sl::Boolean::eFalse; - slConstants.motionVectorsInvalidValue = FLT_MIN; - slConstants.orthographicProjection = sl::Boolean::eFalse; - slConstants.motionVectorsDilated = sl::Boolean::eFalse; - slConstants.motionVectorsJittered = sl::Boolean::eFalse; - - if (SL_FAILED(res, slSetConstants(slConstants, *frameToken, viewport))) { - logger::error("[Streamline] Could not set constants"); - } + auto& upscaling = globals::features::upscaling; + auto jitter = upscaling.jitter; + slConstants.jitterOffset = { -jitter.x, -jitter.y }; + slConstants.reset = sl::Boolean::eFalse; + + slConstants.mvecScale = { 1.0f, 1.0f }; + slConstants.motionVectors3D = sl::Boolean::eFalse; + slConstants.motionVectorsInvalidValue = FLT_MIN; + slConstants.orthographicProjection = sl::Boolean::eFalse; + slConstants.motionVectorsDilated = sl::Boolean::eFalse; + slConstants.motionVectorsJittered = sl::Boolean::eFalse; + + if (SL_FAILED(res, slSetConstants(slConstants, *frameToken, p_viewport))) { + logger::error("[Streamline] Could not set constants for eye {}", eyeIndex); } } -void Streamline::SetDLSSOptions() +void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) { sl::DLSSOptions dlssOptions{}; @@ -277,9 +298,21 @@ void Streamline::SetDLSSOptions() auto state = globals::state; - dlssOptions.outputWidth = (uint)state->screenSize.x; + dlssOptions.outputWidth = width; dlssOptions.outputHeight = (uint)state->screenSize.y; - dlssOptions.colorBuffersHDR = sl::Boolean::eTrue; + + // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 + { + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + D3D11_TEXTURE2D_DESC mainDesc; + static_cast(main.texture)->GetDesc(&mainDesc); + bool isHDR = (mainDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT || + mainDesc.Format == DXGI_FORMAT_R32G32B32A32_FLOAT || + mainDesc.Format == DXGI_FORMAT_R16G16B16A16_TYPELESS || + mainDesc.Format == DXGI_FORMAT_R32G32B32A32_TYPELESS); + dlssOptions.colorBuffersHDR = isHDR ? sl::Boolean::eTrue : sl::Boolean::eFalse; + } dlssOptions.useAutoExposure = sl::Boolean::eTrue; dlssOptions.dlaaPreset = sl::DLSSPreset::ePresetJ; @@ -292,54 +325,128 @@ void Streamline::SetDLSSOptions() dlssOptions.preExposure = 1.0f; dlssOptions.sharpness = 0.0f; - if (SL_FAILED(result, slDLSSSetOptions(viewport, dlssOptions))) { + if (SL_FAILED(result, slDLSSSetOptions(p_viewport, dlssOptions))) { logger::critical("[Streamline] Could not enable DLSS"); } } -void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors) +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) { - CheckFrameConstants(); - SetDLSSOptions(); + auto context = globals::d3d::context; + + sl::Resource colorInRes = { sl::ResourceType::eTex2d, colorIn, 0 }; + sl::Resource colorOutRes = { sl::ResourceType::eTex2d, colorOut, 0 }; + sl::Resource depthRes = { sl::ResourceType::eTex2d, depth, 0 }; + sl::Resource mvecRes = { sl::ResourceType::eTex2d, mvec, 0 }; + sl::Resource reactiveMaskRes = { sl::ResourceType::eTex2d, reactiveMask, 0 }; + sl::Resource transparencyMaskRes = { sl::ResourceType::eTex2d, transparencyMask, 0 }; + + CheckFrameConstants(vp, eyeIndex); + SetDLSSOptions(vp, outputWidth); + + sl::ResourceTag tags[] = { + { &colorInRes, sl::kBufferTypeScalingInputColor, sl::ResourceLifecycle::eOnlyValidNow, &extentIn }, + { &colorOutRes, sl::kBufferTypeScalingOutputColor, sl::ResourceLifecycle::eOnlyValidNow, &extentOut }, + { &depthRes, sl::kBufferTypeDepth, sl::ResourceLifecycle::eValidUntilPresent, &extentIn }, + { &mvecRes, sl::kBufferTypeMotionVectors, sl::ResourceLifecycle::eValidUntilPresent, &extentIn }, + { &reactiveMaskRes, sl::kBufferTypeBiasCurrentColorHint, sl::ResourceLifecycle::eValidUntilPresent, &extentIn }, + { &transparencyMaskRes, sl::kBufferTypeTransparencyHint, sl::ResourceLifecycle::eValidUntilPresent, &extentIn } + }; + + slSetTag(vp, tags, _countof(tags), context); + + // Allocate resources on first use (VR: per-viewport; non-VR: once) + bool isVR = globals::game::isVR; + bool* allocated = isVR ? &globals::features::upscaling.vrResourcesAllocated[eyeIndex] : &resourcesAllocated; + if (!*allocated) { + sl::Result allocResult = slAllocateResources(context, sl::kFeatureDLSS, vp); + if (allocResult != sl::Result::eOk) { + logger::error("[Streamline] slAllocateResources failed{}", isVR ? std::format(" for eye {}", eyeIndex) : ""); + } else { + *allocated = true; + if (isVR) + logger::info("[Streamline] Allocated DLSS resources for eye {}", eyeIndex); + } + } - auto state = globals::state; + sl::ViewportHandle view(vp); + const sl::BaseStructure* inputs[] = { &view }; - auto renderer = globals::game::renderer; - auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + auto state = globals::state; + if (state->frameAnnotations) { + if (globals::game::isVR) { + char buf[32]; + snprintf(buf, sizeof(buf), "DLSS Evaluate Eye %u", eyeIndex); + state->BeginPerfEvent(buf); + } else { + state->BeginPerfEvent("DLSS Evaluate"); + } + } - { - auto screenSize = state->screenSize; - auto renderSize = Util::ConvertToDynamic(screenSize); + sl::Result evalResult = slEvaluateFeature(sl::kFeatureDLSS, *frameToken, inputs, _countof(inputs), context); - sl::Extent lowResExtent{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; - sl::Extent fullExtent{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; + if (state->frameAnnotations) + state->EndPerfEvent(); - sl::Resource colorIn = { sl::ResourceType::eTex2d, a_upscalingTexture, 0 }; - sl::Resource colorOut = { sl::ResourceType::eTex2d, a_upscalingTexture, 0 }; - sl::Resource depth = { sl::ResourceType::eTex2d, depthTexture.texture, 0 }; - sl::Resource mvec = { sl::ResourceType::eTex2d, a_motionVectors, 0 }; + if (evalResult != sl::Result::eOk) { + static bool evalErrorLogged[2] = { false, false }; + uint32_t logIdx = isVR ? eyeIndex : 0; + if (!evalErrorLogged[logIdx]) { + evalErrorLogged[logIdx] = true; + logger::error("[Streamline] slEvaluateFeature failed{} result={}", isVR ? std::format(" for eye {}", eyeIndex) : "", (int)evalResult); + } + } +} - sl::ResourceTag colorInTag = sl::ResourceTag{ &colorIn, sl::kBufferTypeScalingInputColor, sl::ResourceLifecycle::eOnlyValidNow, &lowResExtent }; - sl::ResourceTag colorOutTag = sl::ResourceTag{ &colorOut, sl::kBufferTypeScalingOutputColor, sl::ResourceLifecycle::eOnlyValidNow, &fullExtent }; - sl::ResourceTag depthTag = sl::ResourceTag{ &depth, sl::kBufferTypeDepth, sl::ResourceLifecycle::eValidUntilPresent, &lowResExtent }; - sl::ResourceTag mvecTag = sl::ResourceTag{ &mvec, sl::kBufferTypeMotionVectors, sl::ResourceLifecycle::eValidUntilPresent, &lowResExtent }; +void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors) +{ + auto state = globals::state; - sl::Resource reactiveMask = { sl::ResourceType::eTex2d, a_reactiveMask, 0 }; - sl::ResourceTag reactiveMaskTag = sl::ResourceTag{ &reactiveMask, sl::kBufferTypeBiasCurrentColorHint, sl::ResourceLifecycle::eValidUntilPresent, &lowResExtent }; + auto renderer = globals::game::renderer; + auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - sl::Resource transparencyCompositionMask = { sl::ResourceType::eTex2d, a_transparencyCompositionMask, 0 }; - sl::ResourceTag transparencyCompositionMaskTag = sl::ResourceTag{ &transparencyCompositionMask, sl::kBufferTypeTransparencyHint, sl::ResourceLifecycle::eValidUntilPresent, &lowResExtent }; + auto screenSize = state->screenSize; + auto renderSize = Util::ConvertToDynamic(screenSize); - sl::ResourceTag resourceTags[] = { colorInTag, colorOutTag, depthTag, mvecTag, reactiveMaskTag, transparencyCompositionMaskTag }; + // VR: Combined-buffer mode with extent offsets causes temporal ghosting on the right eye + // because DLSS's internal history buffers use extent offsets as indices. + // Per-eye isolation with extents at {0,0} is required. + if (globals::game::isVR) { + auto& upscaling = globals::features::upscaling; + uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); + uint32_t eyeHeightOut = (uint32_t)screenSize.y; + uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); + uint32_t eyeHeightIn = (uint32_t)renderSize.y; + + upscaling.PreparePerEyeInputs(a_upscalingTexture, depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask); + + for (uint32_t i = 0; i < 2; ++i) { + sl::ViewportHandle vp = (i == 1) ? viewportRight : viewport; + sl::Extent extentIn{ 0, 0, eyeWidthIn, eyeHeightIn }; + sl::Extent extentOut{ 0, 0, eyeWidthOut, eyeHeightOut }; + + EvaluateDLSS(vp, i, + upscaling.vrIntermediateColorIn[i]->resource.get(), upscaling.vrIntermediateColorOut[i]->resource.get(), + upscaling.vrIntermediateDepth[i]->resource.get(), upscaling.vrIntermediateMotionVectors[i]->resource.get(), + upscaling.vrIntermediateReactiveMask[i]->resource.get(), upscaling.vrIntermediateTransparencyMask[i]->resource.get(), + extentIn, extentOut, eyeWidthOut); + } - slSetTag(viewport, resourceTags, _countof(resourceTags), globals::d3d::context); + upscaling.FinalizePerEyeOutputs(a_upscalingTexture); + } else { + // Non-VR: Simple full-texture upscale + sl::Extent extentIn{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; + sl::Extent extentOut{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; + + EvaluateDLSS(viewport, 0, + a_upscalingTexture, a_upscalingTexture, + depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask, + extentIn, extentOut, (uint)screenSize.x); } - - sl::ViewportHandle view(viewport); - const sl::BaseStructure* inputs[] = { &view }; - slEvaluateFeature(sl::kFeatureDLSS, *frameToken, inputs, _countof(inputs), globals::d3d::context); } - /** * @brief Releases DLSS resources and disables DLSS for the current viewport. * @@ -351,4 +458,7 @@ void Streamline::DestroyDLSSResources() dlssOptions.mode = sl::DLSSMode::eOff; slDLSSSetOptions(viewport, dlssOptions); slFreeResources(sl::kFeatureDLSS, viewport); + + // Non-VR resource tracking + resourcesAllocated = false; } diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index dc2adfa346..17d8162ffa 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -33,6 +33,7 @@ class Streamline bool featureDLSS = false; sl::ViewportHandle viewport{ 0 }; + sl::ViewportHandle viewportRight{ 1 }; static constexpr uint32_t MAX_RESOLUTION = 8192; HMODULE interposer = NULL; @@ -63,6 +64,14 @@ class Streamline Util::FrameChecker frameChecker; sl::FrameToken* frameToken = nullptr; + bool resourcesAllocated = false; // Non-VR resource allocation tracking + + // Helper: Execute DLSS for a single viewport with given resources + 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); + // Cached DLL version info for Streamline plugin directory static std::vector> dllVersions; @@ -72,9 +81,9 @@ class Streamline void PostDevice(); - void CheckFrameConstants(); + void CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex = 0); - void SetDLSSOptions(); + void SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width); void Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors); diff --git a/src/Utils/UI.h b/src/Utils/UI.h index f3196ad17a..151efdd6f9 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -16,12 +16,23 @@ struct ImVec2; class Menu; class Feature; -#define BUFFER_VIEWER_NODE(a_value, a_scale) \ - if (ImGui::TreeNode(#a_value)) { \ - ImGui::Image(a_value->srv.get(), { a_value->desc.Width * a_scale, a_value->desc.Height * a_scale }); \ - ImGui::TreePop(); \ +// Helper macro for displaying texture buffers in ImGui with resolution info +#define BUFFER_VIEWER_NODE_IMPL(a_value, a_label, a_scale) \ + if (a_value && a_value->srv.get()) { \ + char buf[128]; \ + snprintf(buf, sizeof(buf), "%s (%ux%u)", a_label, a_value->desc.Width, a_value->desc.Height); \ + if (ImGui::TreeNode(buf)) { \ + ImGui::Image(a_value->srv.get(), { a_value->desc.Width * a_scale, a_value->desc.Height * a_scale }); \ + ImGui::TreePop(); \ + } \ } +#define BUFFER_VIEWER_NODE(a_value, a_scale) \ + BUFFER_VIEWER_NODE_IMPL(a_value, #a_value, a_scale) + +#define BUFFER_VIEWER_NODE_TITLE(a_value, a_title, a_scale) \ + BUFFER_VIEWER_NODE_IMPL(a_value, a_title, a_scale) + #define BUFFER_VIEWER_NODE_BULLET(a_value, a_scale) \ ImGui::BulletText(#a_value); \ ImGui::Image(a_value->srv.get(), { a_value->desc.Width * a_scale, a_value->desc.Height * a_scale }); From 4f463ac8334e75b4e9818f26551712e5002dc4e7 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 5 Feb 2026 00:43:16 -0800 Subject: [PATCH 2/2] chore: address ai comments --- src/Features/Upscaling.cpp | 4 +++- src/Features/Upscaling/FidelityFX.cpp | 2 ++ src/Features/Upscaling/Streamline.cpp | 9 ++++++++- src/Utils/UI.h | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 8bf23a836b..cec286bffb 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -849,7 +849,9 @@ void Upscaling::PreparePerEyeInputs(ID3D11Resource* colorSrc, ID3D11Resource* de bool needsRecreate = !vrIntermediateColorIn[0] || !vrIntermediateColorOut[0]; if (!needsRecreate) { needsRecreate = (vrIntermediateColorIn[0]->desc.Width != eyeWidthIn || - vrIntermediateColorIn[0]->desc.Height != eyeHeightIn); + vrIntermediateColorIn[0]->desc.Height != eyeHeightIn || + vrIntermediateColorOut[0]->desc.Width != eyeWidthOut || + vrIntermediateColorOut[0]->desc.Height != eyeHeightOut); } if (needsRecreate) { logger::info("[Upscaling] (Re)creating VR intermediates: per-eye in {}x{}, out {}x{}", diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index c471c25187..66ea4d44e9 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -232,6 +232,8 @@ void FidelityFX::CreateFSRResources() if (ffxFsr3ContextCreate(&fsrContext[i], &contextDescription) != FFX_OK) { logger::critical("[FidelityFX] Failed to initialize FSR3 context for eye {}!", i); + for (uint32_t j = 0; j < i; ++j) + ffxFsr3ContextDestroy(&fsrContext[j]); free(fsrScratchBuffer); fsrScratchBuffer = nullptr; return; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index cecb137532..e9dcd4e8de 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -456,9 +456,16 @@ void Streamline::DestroyDLSSResources() { sl::DLSSOptions dlssOptions{}; dlssOptions.mode = sl::DLSSMode::eOff; + slDLSSSetOptions(viewport, dlssOptions); slFreeResources(sl::kFeatureDLSS, viewport); - // Non-VR resource tracking + if (globals::game::isVR) { + slDLSSSetOptions(viewportRight, dlssOptions); + slFreeResources(sl::kFeatureDLSS, viewportRight); + globals::features::upscaling.vrResourcesAllocated[0] = false; + globals::features::upscaling.vrResourcesAllocated[1] = false; + } + resourcesAllocated = false; } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 151efdd6f9..566f6aa8f5 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1,6 +1,7 @@ #pragma once #include #include // For FLT_MAX +#include #include #include #include