Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions package/Shaders/Common/FoveatedMask.hlsli
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef FOVEATED_MASK_HLSLI
#define FOVEATED_MASK_HLSLI

// Superellipse (shape power 4 = squircle) foveation mask: a smooth center->periphery weight that
// avoids the hard seam of a rect mask. CPU mirror of the clamping is in src/Features/FoveatedCommon.h.

#ifndef FOVEATED_CENTER_SCALE_MIN
# define FOVEATED_CENTER_SCALE_MIN 0.25
#endif
#ifndef FOVEATED_CENTER_SCALE_MAX
# define FOVEATED_CENTER_SCALE_MAX 1.0
#endif
#ifndef FOVEATED_CENTER_FEATHER_MIN
# define FOVEATED_CENTER_FEATHER_MIN 1e-4
#endif
#ifndef FOVEATED_CENTER_HORIZONTAL_SCALE_MIN
# define FOVEATED_CENTER_HORIZONTAL_SCALE_MIN 1.0
#endif
#ifndef FOVEATED_CENTER_HORIZONTAL_SCALE_MAX
# define FOVEATED_CENTER_HORIZONTAL_SCALE_MAX 2.0
#endif
#ifndef FOVEATED_MASK_SHAPE_POWER
# define FOVEATED_MASK_SHAPE_POWER 4
#endif

float FoveatedClampCenterScale(float centerScale)
{
return clamp(centerScale, FOVEATED_CENTER_SCALE_MIN, FOVEATED_CENTER_SCALE_MAX);
}

float2 FoveatedComputeCenterUV(float2 centerOffset)
{
return clamp(float2(0.5, 0.5) + centerOffset, float2(0.0, 0.0), float2(1.0, 1.0));
}

float FoveatedClampCenterHorizontalScale(float centerHorizontalScale)
{
return clamp(centerHorizontalScale, FOVEATED_CENTER_HORIZONTAL_SCALE_MIN, FOVEATED_CENTER_HORIZONTAL_SCALE_MAX);
}

float2 FoveatedComputeMaskRadii(float centerScale, float centerHorizontalScale)
{
float clampedCenterScale = FoveatedClampCenterScale(centerScale);
float clampedCenterHorizontalScale = FoveatedClampCenterHorizontalScale(centerHorizontalScale);
float2 radii = float2(clampedCenterScale * clampedCenterHorizontalScale * 0.5, clampedCenterScale * 0.5);
return max(radii, FOVEATED_CENTER_FEATHER_MIN.xx);
}

float FoveatedComputeNormalizedFeather(float centerScale, float centerFeather, float centerHorizontalScale)
{
float2 radii = FoveatedComputeMaskRadii(centerScale, centerHorizontalScale);
float baseRadius = max(min(radii.x, radii.y), FOVEATED_CENTER_FEATHER_MIN);
return max(centerFeather, FOVEATED_CENTER_FEATHER_MIN) / baseRadius;
}

float FoveatedComputeMaskDistance(float2 eyeUv, float centerScale, float centerHorizontalScale, float2 centerOffset)
{
float2 radii = FoveatedComputeMaskRadii(centerScale, centerHorizontalScale);
float2 centerUv = FoveatedComputeCenterUV(centerOffset);
float2 normalized = (eyeUv - centerUv) / radii;
float2 absoluteNormalized = abs(normalized);
#if FOVEATED_MASK_SHAPE_POWER == 4
// Squircle fast path: pow(t,4) == (t*t)^2 and pow(sum,1/4) == sqrt(sqrt(sum)),
// so the default shape avoids three transcendental pow() calls per pixel in
// the SSR path that is explicitly trying to save GPU time.
float2 sq = absoluteNormalized * absoluteNormalized;
float pNorm = sq.x * sq.x + sq.y * sq.y;
return sqrt(sqrt(max(pNorm, 0.0)));
#else
float shapePower = max((float)FOVEATED_MASK_SHAPE_POWER, 1.0);
float invShapePower = 1.0 / shapePower;
float pNorm = pow(absoluteNormalized.x, shapePower) + pow(absoluteNormalized.y, shapePower);
return pow(max(pNorm, 0.0), invShapePower);
Comment thread
alandtse marked this conversation as resolved.
#endif
}

float FoveatedComputeCenterBlendWeight(float2 eyeUv, float centerScale, float centerFeather, float centerHorizontalScale, float2 centerOffset)
{
float normalizedFeather = FoveatedComputeNormalizedFeather(centerScale, centerFeather, centerHorizontalScale);
float maskDistance = FoveatedComputeMaskDistance(eyeUv, centerScale, centerHorizontalScale, centerOffset);
return 1.0 - smoothstep(1.0, 1.0 + normalizedFeather, maskDistance);
}

#endif
32 changes: 32 additions & 0 deletions package/Shaders/Common/FoveatedShaderDetail.hlsli
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#ifndef FOVEATED_SHADER_DETAIL_HLSLI
#define FOVEATED_SHADER_DETAIL_HLSLI

// Per-pixel detail weight for foveated effects: pass the mode (0=off/1=feathered/2=hard, see
// FoveatedCommon::GetShaderMode) + mask params, get a 0..1 weight (0 outside the mask = skip).

#include "Common/FoveatedMask.hlsli"

static const float FOVEATED_SHADER_DETAIL_MODE_FEATHERED = 1.0;
static const float FOVEATED_SHADER_DETAIL_MODE_HARD_CUTOFF = 2.0;

float FoveatedEvaluateShaderDetailWeight(float mode, float2 eyeUv, float centerScale, float centerFeather, float centerHorizontalScale, float2 centerOffset)
{
bool detailModeEnabled = mode >= FOVEATED_SHADER_DETAIL_MODE_FEATHERED;
if (!detailModeEnabled)
return 1.0f;

bool hardCutoffMode = mode >= FOVEATED_SHADER_DETAIL_MODE_HARD_CUTOFF;
if (hardCutoffMode) {
float edgeDistance = FoveatedComputeMaskDistance(eyeUv, centerScale, centerHorizontalScale, centerOffset);
return edgeDistance > 1.0f ? 0.0f : 1.0f;
}

return FoveatedComputeCenterBlendWeight(eyeUv, centerScale, centerFeather, centerHorizontalScale, centerOffset);
}

bool FoveatedIsShaderDetailActive(float detailWeight)
{
return detailWeight > 0.0001f;
}

#endif
2 changes: 2 additions & 0 deletions package/Shaders/Common/SharedData.hlsli
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ namespace SharedData
float4 AmbientSHR;
float4 AmbientSHG;
float4 AmbientSHB;
float4 VRFoveationData0; // x=center coverage scale, y=feather, z=horizontal scale, w=SSR raymarch mode (0 off, 1 feathered, 2 hard cutoff)
float4 VRFoveationCenterOffsets; // xy=left eye center offset, zw=right eye center offset
float4 HDRData;
Comment thread
alandtse marked this conversation as resolved.
};

Expand Down
92 changes: 87 additions & 5 deletions package/Shaders/ISReflectionsRayTracing.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ static const int binaryIterations = ceil(log2(iterations));

static const float rayLength = 1.0;

# if defined(VR)
# include "Common/FoveatedShaderDetail.hlsli"

static const int minFoveatedIterations = 16;

// Per-pixel SSR foveation weight from the active foveation mask (VRFoveationData0
// + per-eye center offset). 1 in the center, falling to 0 in the periphery.
float GetVRSSRFoveationWeight(float ssrFoveationMode, float2 eyeUv, uint eyeIndex)
{
float2 centerOffset = eyeIndex == 0 ? SharedData::VRFoveationCenterOffsets.xy : SharedData::VRFoveationCenterOffsets.zw;
return FoveatedEvaluateShaderDetailWeight(
ssrFoveationMode,
eyeUv,
SharedData::VRFoveationData0.x,
SharedData::VRFoveationData0.y,
SharedData::VRFoveationData0.z,
centerOffset);
}

// Scale the raymarch count by the foveation weight: full in the center, down to
// minFoveatedIterations toward the periphery.
int GetSSRRaymarchIterations(float foveationWeight)
{
int iterationCount = (int)ceil(lerp((float)minFoveatedIterations, (float)iterations, saturate(foveationWeight)));
return min(max(iterationCount, minFoveatedIterations), iterations);
}

int GetSSRBinaryIterations(int raymarchIterations)
{
int iterationCount = (int)ceil(log2((float)raymarchIterations));
return min(max(iterationCount, 1), binaryIterations);
}
# endif

float2 ConvertRaySample(float2 raySample, uint eyeIndex)
{
return FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(Stereo::ConvertToStereoUV(raySample, eyeIndex));
Expand All @@ -46,14 +80,34 @@ float2 ConvertRaySamplePrevious(float2 raySample, uint eyeIndex)
float4 GetReflectionColor(
float3 projReflectionDirection,
float3 projPosition,
uint eyeIndex)
uint eyeIndex
# if defined(VR)
,
int raymarchIterations,
int binaryIterationsCount,
float foveationWeight
# endif
)
{
float3 prevRaySample;
float3 raySample = projPosition;

for (int i = 0; i < iterations; i++) {
// VR scales the raymarch/binary counts by the foveation weight and fades the result; non-VR uses
// full counts. Bounds are runtime in VR, so [loop] is required (cannot unroll).
# if defined(VR)
int rayCount = raymarchIterations;
int binCount = binaryIterationsCount;
float fovWeight = foveationWeight;
[loop] for (int i = 0; i < rayCount; i++)
{
# else
int rayCount = iterations;
int binCount = binaryIterations;
float fovWeight = 1.0;
for (int i = 0; i < rayCount; i++) {
Comment thread
alandtse marked this conversation as resolved.
# endif
prevRaySample = raySample;
raySample = projPosition + (float(i) / float(iterations)) * projReflectionDirection;
raySample = projPosition + (float(i) / float(rayCount)) * projReflectionDirection;

float2 sampleUV;
uint sampleEyeIndex;
Expand All @@ -71,7 +125,12 @@ float4 GetReflectionColor(
float depthThicknessFactor;
uint hitEyeIndex = sampleEyeIndex;

for (int k = 0; k < binaryIterations; k++) {
# if defined(VR)
[loop] for (int k = 0; k < binCount; k++)
{
# else
for (int k = 0; k < binCount; k++) {
# endif
binaryRaySample = lerp(binaryMinRaySample, binaryMaxRaySample, 0.5);

Stereo::ResolveMonoUVForEye(binaryRaySample, eyeIndex, sampleUV, hitEyeIndex);
Expand Down Expand Up @@ -134,7 +193,7 @@ float4 GetReflectionColor(
alpha = float4(AlphaTex.SampleLevel(AlphaSampler, ConvertRaySamplePrevious(reprojectedRaySample.xy, finalEyeIndex), 0).xyz, 1.0);

float3 reflectionColor = color + SSRParams.z * alpha.xyz * alpha.w;
return float4(reflectionColor, fadeFactor);
return float4(reflectionColor, fadeFactor * fovWeight);
}

return 0.0;
Expand All @@ -160,6 +219,18 @@ PS_OUTPUT main(PS_INPUT input)

uv = Stereo::ConvertFromStereoUV(uv, eyeIndex);

# if defined(VR)
float ssrFoveationWeight = 1.0;
float ssrFoveationMode = SharedData::VRFoveationData0.w;
[branch] if (ssrFoveationMode >= FOVEATED_SHADER_DETAIL_MODE_FEATHERED)
{
ssrFoveationWeight = GetVRSSRFoveationWeight(ssrFoveationMode, uv, eyeIndex);
// Outside the foveation mask: skip SSR entirely. The cubemap/water
// reflection fallback already covers these pixels.
[branch] if (!FoveatedIsShaderDetailActive(ssrFoveationWeight)) return psout;
}
# endif

[branch] if (NormalTex.Sample(NormalSampler, screenPosition).z <= 0)
{
return psout;
Expand Down Expand Up @@ -191,7 +262,18 @@ PS_OUTPUT main(PS_INPUT input)
float3 projPosition = float3(uv, depth);
float3 projReflectionDirection = normalize(projReflectionPosition.xyz - projPosition) * rayLength;

# if defined(VR)
int raymarchIterations = iterations;
int binaryIterationsCount = binaryIterations;
[branch] if (ssrFoveationWeight < 0.9999)
{
raymarchIterations = GetSSRRaymarchIterations(ssrFoveationWeight);
binaryIterationsCount = GetSSRBinaryIterations(raymarchIterations);
}
psout.Color = GetReflectionColor(projReflectionDirection, projPosition, eyeIndex, raymarchIterations, binaryIterationsCount, ssrFoveationWeight);
# else
psout.Color = GetReflectionColor(projReflectionDirection, projPosition, eyeIndex);
# endif

return psout;
}
Expand Down
66 changes: 66 additions & 0 deletions src/Features/FoveatedCommon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once

#include <algorithm>
#include <cmath>

// Shared CPU-side helpers for VR foveated shader detail; GPU mirror in FoveatedMask.hlsli, with
// GetShaderMode's 0/1/2 encoding as the contract. Only SSR-consumed pieces exist (compute helpers omitted).
namespace FoveatedCommon
{
constexpr float kCenterScaleMin = 0.25f;
constexpr float kCenterScaleMax = 1.0f;
constexpr float kCenterFeather = 0.05f;
constexpr float kCenterHorizontalScaleMin = 1.0f;
constexpr float kCenterHorizontalScaleMax = 2.0f;
constexpr float kFullCoverageThreshold = 0.999f;

enum class DetailMode
{
Off,
Feathered,
HardCutoff
};

constexpr DetailMode GetDetailMode(bool a_enabled, bool a_hardCutoff)
{
if (!a_enabled)
return DetailMode::Off;
return a_hardCutoff ? DetailMode::HardCutoff : DetailMode::Feathered;
}

// 0 = off, 1 = feathered, 2 = hard cutoff. Must match the
// FOVEATED_SHADER_DETAIL_MODE_* constants in FoveatedShaderDetail.hlsli.
constexpr float GetShaderMode(DetailMode a_mode)
{
switch (a_mode) {
case DetailMode::Feathered:
return 1.0f;
case DetailMode::HardCutoff:
return 2.0f;
case DetailMode::Off:
default:
return 0.0f;
}
}

inline float ClampCenterScale(float a_value)
{
if (!std::isfinite(a_value))
return kCenterScaleMax;
return std::clamp(a_value, kCenterScaleMin, kCenterScaleMax);
}

// A near-full center means foveation does nothing useful — callers skip the
// per-pixel mask in that case to avoid paying its cost for no saving.
inline bool IsActiveCoverage(float a_centerScale)
{
return ClampCenterScale(a_centerScale) < kFullCoverageThreshold;
}

inline float ClampCenterHorizontalScale(float a_value)
{
if (!std::isfinite(a_value))
return 1.0f;
return std::clamp(a_value, kCenterHorizontalScaleMin, kCenterHorizontalScaleMax);
}
}
35 changes: 35 additions & 0 deletions src/Features/Upscaling/FoveatedRender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "../../Globals.h"
#include "../../Utils/Subrect.h"
#include "../../Utils/UI.h"
#include "../FoveatedCommon.h"
#include "../Upscaling.h"
#include "FoveatedRender/Core.h"

Expand Down Expand Up @@ -118,6 +119,40 @@ bool FoveatedRender::IsRuntimeSupported() const
return globals::game::isVR && globals::features::upscaling.streamline.featureDLSS;
}

FoveatedRender::FoveationProfile FoveatedRender::GetFoveationProfile() const
{
FoveationProfile profile;
if (!IsActive())
return profile;

const auto& leftUV = subrectController.GetUV();
const auto& rightUV = subrectController.GetRightEyeUV();

// Map the rectangular subrect onto the centered superellipse the mask helper expects: vertical
// extent drives coverageScale (radiusY = coverageScale/2), the rect aspect drives the horizontal
// stretch (radiusX = coverageScale * hScale/2). The mask carries one scale for both eyes (only
// the center offset is per-eye), so size comes from the less-foveated (larger) extent of the two:
// the center is the superset enclosing both eyes' full-quality regions, so neither eye's sharp
// zone is ever foveated (min would shrink it below an eye's sharp region and foveate it). A
// full eye therefore yields full coverage and disables foveation (the gate below).
const float coverageH = std::max(leftUV.h, rightUV.h);
const float coverageW = std::max(leftUV.w, rightUV.w);
const float coverageScale = FoveatedCommon::ClampCenterScale(coverageH);

// Availability keys off the clamped scale: if the larger eye rounds up to full coverage there is
// nothing to foveate, leave the default (available == false).
if (!FoveatedCommon::IsActiveCoverage(coverageScale))
return profile;

profile.available = true;
profile.coverageScale = coverageScale;
profile.centerHorizontalScale = FoveatedCommon::ClampCenterHorizontalScale(
coverageH > 1e-4f ? coverageW / coverageH : 1.0f);
profile.centerOffsets[0] = { (leftUV.x + leftUV.w * 0.5f) - 0.5f, (leftUV.y + leftUV.h * 0.5f) - 0.5f };
profile.centerOffsets[1] = { (rightUV.x + rightUV.w * 0.5f) - 0.5f, (rightUV.y + rightUV.h * 0.5f) - 0.5f };
return profile;
Comment thread
alandtse marked this conversation as resolved.
Comment thread
alandtse marked this conversation as resolved.
}

void FoveatedRender::LatchQualityMode()
{
qualityModeAtBoot = std::clamp(globals::features::upscaling.settings.qualityMode, 1u, 4u);
Expand Down
Loading
Loading