
#include "/lib/noise_render_scale.glsl"

// Temporal reconstruction follows the actual scene viewport. At 67% scale
// this dispatches roughly 45% as many invocations as a display-sized resolve.
const vec2 workGroupsRender = vec2(NOISE_RENDER_SCALE, NOISE_RENDER_SCALE);
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

uniform sampler2D colortex3;
uniform sampler2D colortex6;
uniform sampler2D colortex7;
uniform sampler2D depthtex0;
uniform sampler2D depthtex1;
uniform sampler2D depthtex2;
uniform sampler2D noise_tsr_depth_sampler;

uniform int frameCounter;
uniform vec3 cameraPosition;
uniform vec3 previousCameraPosition;
uniform mat4 gbufferProjection;
uniform mat4 gbufferProjectionInverse;
uniform mat4 gbufferModelView;
uniform mat4 gbufferModelViewInverse;
uniform mat4 gbufferPreviousProjection;
uniform mat4 gbufferPreviousModelView;

layout(rgba16f) writeonly uniform image2D noise_fsr_img;

#include "/lib/noise_antialiasing.glsl"
#include "/lib/noise_temporal.glsl"

float noiseLinearViewDepth(float depth) {
	vec4 viewDepth = gbufferProjectionInverse *
		vec4(0.0, 0.0, depth * 2.0 - 1.0, 1.0);
	float safeW = abs(viewDepth.w) > 1e-6
		? viewDepth.w : (viewDepth.w < 0.0 ? -1e-6 : 1e-6);
	return abs(viewDepth.z / safeW);
}

vec4 noiseTemporalResolve(ivec2 outputSize, float currentDepth,
	ivec2 sourcePixel, ivec2 viewportSize, vec2 depthLogicalUv, vec3 currentColor) {
	float encodedCount = noiseTemporalEncodeSamples(1.0);
	#if AA_MODE != 2 || TEMPORAL_FRAMES <= 1
	return vec4(currentColor, encodedCount);
	#else
	bool skyPixel = currentDepth >= 0.999999;
	vec4 previousClip;
	if (skyPixel) {
		// Depth has no position for the sky. Reproject its world-space direction
		// so camera rotation is exact and translation cannot drag the infinitely
		// distant atmosphere. Cloud motion is handled by conservative history
		// confidence and neighbourhood clipping below.
		vec4 viewFar = gbufferProjectionInverse *
			vec4(depthLogicalUv * 2.0 - 1.0, 1.0, 1.0);
		float farW = abs(viewFar.w) > 1e-6
			? viewFar.w : (viewFar.w < 0.0 ? -1e-6 : 1e-6);
		vec3 currentViewDirection = normalize(viewFar.xyz / farW);
		vec3 worldDirection = normalize(mat3(gbufferModelViewInverse) *
			currentViewDirection);
		// Clouds are finite, not part of an infinitely distant sky dome. Reproject
		// through the representative middle of the Y=300-500 height map whenever
		// the ray can reach it;
		// this captures camera translation without needing a separate cloud-depth
		// history target. Rays outside cloud range keep directional reprojection.
		float cloudPlaneDistance = (400.0 - cameraPosition.y) /
			max(worldDirection.y, 1e-5);
		if (worldDirection.y > 0.004 && cloudPlaneDistance > 0.0 &&
			cloudPlaneDistance < 2100.0) {
			vec3 cloudWorldPosition = cameraPosition +
				worldDirection * cloudPlaneDistance;
			vec3 previousCloudPlayerPosition = cloudWorldPosition -
				previousCameraPosition;
			vec4 previousCloudView = gbufferPreviousModelView *
				vec4(previousCloudPlayerPosition, 1.0);
			previousClip = gbufferPreviousProjection * previousCloudView;
		} else {
			vec3 previousViewDirection = mat3(gbufferPreviousModelView) *
				worldDirection;
			previousClip = gbufferPreviousProjection *
				vec4(previousViewDirection, 0.0);
		}
	} else {
		vec4 viewPosition = gbufferProjectionInverse *
			vec4(depthLogicalUv * 2.0 - 1.0,
			currentDepth * 2.0 - 1.0, 1.0);
		float safeW = abs(viewPosition.w) > 1e-6
			? viewPosition.w : (viewPosition.w < 0.0 ? -1e-6 : 1e-6);
		viewPosition /= safeW;
		vec3 absolutePosition = (gbufferModelViewInverse *
			vec4(viewPosition.xyz, 1.0)).xyz + cameraPosition;
		vec3 previousPlayerPosition = absolutePosition - previousCameraPosition;
		vec4 previousViewPosition = gbufferPreviousModelView *
			vec4(previousPlayerPosition, 1.0);
		previousClip = gbufferPreviousProjection * previousViewPosition;
	}

	bool validHistory = previousClip.w > 1e-5 && frameCounter > 1;
	vec3 previousNdc = previousClip.xyz / max(abs(previousClip.w), 1e-6);
	vec2 historyUv = previousNdc.xy * 0.5 + 0.5;
	vec2 halfPixel = 0.5 / vec2(outputSize);
	validHistory = validHistory && all(greaterThan(historyUv, halfPixel));
	validHistory = validHistory && all(lessThan(historyUv, vec2(1.0) - halfPixel));
	validHistory = validHistory && length(cameraPosition - previousCameraPosition) < 8.0;
	float projectionDelta = length(gbufferProjection[0] - gbufferPreviousProjection[0])
		+ length(gbufferProjection[1] - gbufferPreviousProjection[1])
		+ length(gbufferProjection[2] - gbufferPreviousProjection[2]);
	validHistory = validHistory && projectionDelta < 1e-5;

	vec2 safeHistoryUv = clamp(historyUv, halfPixel, vec2(1.0) - halfPixel);
	vec4 historySample = texture(colortex6, safeHistoryUv);
	ivec2 historyPixel = clamp(ivec2(floor(safeHistoryUv * vec2(outputSize))),
		ivec2(0), outputSize - 1);
	if (!skyPixel) {
		float previousDepth = texelFetch(noise_tsr_depth_sampler,
			historyPixel, 0).r;
		float expectedPreviousDepth = previousNdc.z * 0.5 + 0.5;
		float previousViewDepth = noiseLinearViewDepth(previousDepth);
		float expectedPreviousViewDepth = noiseLinearViewDepth(expectedPreviousDepth);
		float depthTolerance = 0.035 + expectedPreviousViewDepth * 0.0025;
		validHistory = validHistory &&
			abs(previousViewDepth - expectedPreviousViewDepth) < depthTolerance;
	}

	vec3 neighborhoodMin = currentColor;
	vec3 neighborhoodMax = currentColor;
	vec3 neighborhoodAverage = currentColor;
	float neighborhoodWeight = 1.0;
	float currentViewDepth = noiseLinearViewDepth(currentDepth);
	float closestDepth = currentDepth;
	float farthestDepth = currentDepth;
	const ivec2 neighborhoodOffsets[4] = ivec2[4](
		ivec2(-1, 0), ivec2(1, 0), ivec2(0, -1), ivec2(0, 1));
	for (int tapIndex = 0; tapIndex < 4; ++tapIndex) {
		ivec2 tapPixel = clamp(sourcePixel + neighborhoodOffsets[tapIndex],
			ivec2(0), viewportSize - 1);
		vec4 tap = texelFetch(colortex7, tapPixel, 0);
		float tapDepth = texelFetch(depthtex0, tapPixel, 0).r;
		float tapViewDepth = noiseLinearViewDepth(tapDepth);
		float sameSurfaceTolerance = 0.035 + currentViewDepth * 0.0025;
		if (abs(tapViewDepth - currentViewDepth) < sameSurfaceTolerance) {
			neighborhoodMin = min(neighborhoodMin, tap.rgb);
			neighborhoodMax = max(neighborhoodMax, tap.rgb);
			neighborhoodAverage += tap.rgb;
			neighborhoodWeight += 1.0;
		}
		closestDepth = min(closestDepth, tapDepth);
		farthestDepth = max(farthestDepth, tapDepth);
	}
	neighborhoodAverage /= neighborhoodWeight;
	vec3 clampMargin = (neighborhoodMax - neighborhoodMin) * 0.06 + vec3(0.001);
	vec3 historyColor = clamp(historySample.rgb,
		neighborhoodMin - clampMargin, neighborhoodMax + clampMargin);

	float motion = length(historyUv - depthLogicalUv);
	float motionConfidence = 1.0 - smoothstep(0.0015, 0.055, motion);
	float colorDifference = length(historyColor - currentColor) /
		max(length(neighborhoodAverage) + 0.08, 0.08);
	float colorConfidence = exp(-colorDifference * 4.0);
	float depthEdge = smoothstep(0.02, 0.75,
		abs(noiseLinearViewDepth(farthestDepth) - noiseLinearViewDepth(closestDepth)));
	vec2 sourceUv = (vec2(sourcePixel) + 0.5) / vec2(outputSize);
	float reactiveMask = texture(colortex3, sourceUv).r;
	float opaqueDepth = texture(depthtex1, sourceUv).r;
	float preHandDepth = texture(depthtex2, sourceUv).r;
	bool translucentPixel = currentDepth + 1e-6 < opaqueDepth;
	bool handPixel = currentDepth + 1e-6 < preHandDepth;
	float responsiveSurface = handPixel ? 0.08 : (translucentPixel ? 0.35 : 1.0);
	float reactiveConfidence = 1.0 - reactiveMask * 0.72;
	// A confirmed opaque surface needs a temporal lock: sub-pixel coverage is
	// expected to change with jitter and must not be mistaken for a scene cut.
	// Depth reprojection still rejects actual disocclusions at silhouettes.
	float lockFloor = mix(0.72, 0.12, max(reactiveMask,
		handPixel ? 1.0 : (translucentPixel ? 0.72 : 0.0)));
	colorConfidence = max(colorConfidence, lockFloor);
	float edgeConfidence = mix(1.0, 0.82, depthEdge);
	// Volumetric clouds are animated transparent content. A shorter effective
	// history integrates ray jitter without leaving long trails in the wind.
	float skyConfidence = skyPixel ? 0.55 : 1.0;
	float historyConfidence = validHistory
		? motionConfidence * colorConfidence * responsiveSurface *
		  reactiveConfidence * edgeConfidence * skyConfidence
		: 0.0;

	vec2 temporalState = noiseTemporalAccumulate(historySample.a,
		historyConfidence);
	return vec4(mix(currentColor, historyColor, temporalState.x), temporalState.y);
	#endif
}

void main() {
	ivec2 sourcePixel = ivec2(gl_GlobalInvocationID.xy);
	ivec2 outputSize = imageSize(noise_fsr_img);
	ivec2 viewportSize = noiseRenderViewportSize(outputSize);
	if (any(greaterThanEqual(sourcePixel, viewportSize))) return;

	float currentDepth = texelFetch(depthtex0, sourcePixel, 0).r;
	vec2 depthLogicalUv = (vec2(sourcePixel) + 0.5) / vec2(viewportSize);
	vec3 currentColor = texelFetch(colortex7, sourcePixel, 0).rgb;
	vec4 temporalColor = noiseTemporalResolve(outputSize, currentDepth,
		sourcePixel, viewportSize, depthLogicalUv, currentColor);
	imageStore(noise_fsr_img, sourcePixel,
		vec4(clamp(temporalColor.rgb, 0.0, 1.0), temporalColor.a));
}
