Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
77 changes: 77 additions & 0 deletions package/Shaders/Common/FoveatedMask.hlsli
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#ifndef FOVEATED_MASK_HLSLI
#define FOVEATED_MASK_HLSLI

// Superellipse (shape power 4 = squircle) foveation mask. Produces a smooth
// radial center->periphery weight that matches peripheral-vision falloff and
// avoids the hard rectangular seam a rect mask would show. CPU mirror of the
// parameter clamping lives 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);
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.
}

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
34 changes: 34 additions & 0 deletions package/Shaders/Common/FoveatedShaderDetail.hlsli
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#ifndef FOVEATED_SHADER_DETAIL_HLSLI
#define FOVEATED_SHADER_DETAIL_HLSLI

// Per-pixel detail weight for foveated effects. Effects pass their mode
// (0=off, 1=feathered, 2=hard cutoff; see FoveatedCommon::GetShaderMode) plus
// the active foveation mask params, and get back a 0..1 weight to scale their
// work by. Outside the mask the weight is 0, letting the effect skip entirely.

#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
93 changes: 88 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,35 @@ 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 foveation reduces the raymarch/binary counts toward the periphery and
// fades the result; non-VR uses the full counts and no fade. The loop bounds
// are runtime values 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 +126,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 +194,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 +220,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 +263,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
72 changes: 72 additions & 0 deletions src/Features/FoveatedCommon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

#include <algorithm>
#include <cmath>

// Shared CPU-side helpers for VR foveated shader detail. The matching GPU math
// lives in package/Shaders/Common/FoveatedMask.hlsli; the 0/1/2 mode encoding
// produced by GetShaderMode is the contract both sides agree on.
//
// Scope note: only the pieces consumed by foveated SSR are present. The
// compute-dispatch helpers (inscribed/bounding rects, thread-group alignment)
// from the upstream fork are intentionally omitted until a compute-shader
// foveation effect needs them.
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);
}
}
38 changes: 38 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,43 @@ 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();

// Disable only when BOTH eyes cover the full eye — if either eye is foveated
// we still build a profile.
const auto isFullEye = [](const Util::Subrect::UVRegion& uv) {
return uv.w >= FoveatedCommon::kFullCoverageThreshold && uv.h >= FoveatedCommon::kFullCoverageThreshold;
};
if (isFullEye(leftUV) && isFullEye(rightUV))
return profile;
Comment thread
alandtse marked this conversation as resolved.

// 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 eyes — the mask then stays inside each eye's full-quality region and
// never foveates a sharp zone. Seeded presets are symmetric in size, so
// left == right and this is a no-op for them. Approximate (rect vs
// superellipse) but sufficient for the per-pixel center/periphery weight.
const float coverageH = std::max(leftUV.h, rightUV.h);
const float coverageW = std::max(leftUV.w, rightUV.w);
profile.available = true;
profile.coverageScale = FoveatedCommon::ClampCenterScale(coverageH);
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