diff --git a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift index 08b4b80b..62aadd44 100644 --- a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift +++ b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift @@ -617,31 +617,6 @@ public func InitDebugPipeline() -> RenderPipeline? { ) } -public func InitTAAResolvePipeline() -> RenderPipeline? { - let wf = renderInfo.colorPipeline.working - return CreatePipeline( - vertexShader: "vertexTAAResolveShader", - fragmentShader: "fragmentTAAResolveShader", - vertexDescriptor: createPostProcessVertexDescriptor(), - colorFormats: [wf.lookOutput], - depthFormat: .invalid, - depthEnabled: false, - name: "TAA Resolve Pipeline" - ) -} - -public func InitVelocityPipeline() -> RenderPipeline? { - CreatePipeline( - vertexShader: "vertexVelocityShader", - fragmentShader: "fragmentVelocityShader", - vertexDescriptor: createPostProcessVertexDescriptor(), - colorFormats: [.rg16Float], - depthFormat: .invalid, - depthEnabled: false, - name: "Velocity Pipeline" - ) -} - public func InitTransparencyPipeline() -> RenderPipeline? { CreatePipeline( vertexShader: "vertexModelShader", @@ -702,8 +677,6 @@ public func DefaultPipeLines() -> [(RenderPipelineType, RenderPipelineInitBlock) (.outputTransform, InitOutputTransformPipeline), (.debug, InitDebugPipeline), (.transparency, InitTransparencyPipeline), - (.velocity, InitVelocityPipeline), - (.taaResolve, InitTAAResolvePipeline), ] } diff --git a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift index 92e54b00..6efb6a2b 100644 --- a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift +++ b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift @@ -52,6 +52,4 @@ public extension RenderPipelineType { static let fxaa: RenderPipelineType = "fxaa" static let transparency: RenderPipelineType = "transparency" static let debug: RenderPipelineType = "debug" - static let velocity: RenderPipelineType = "velocity" - static let taaResolve: RenderPipelineType = "taaResolve" } diff --git a/Sources/UntoldEngine/Renderer/RenderInitializer.swift b/Sources/UntoldEngine/Renderer/RenderInitializer.swift index 26a813ba..5a3ba2bd 100644 --- a/Sources/UntoldEngine/Renderer/RenderInitializer.swift +++ b/Sources/UntoldEngine/Renderer/RenderInitializer.swift @@ -392,16 +392,6 @@ func initRenderPassDescriptors() { ], depthAttachment: nil ) - - // TAA: velocity pass writes rg16Float motion vectors each frame. - renderInfo.velocityRenderPassDescriptor = createRenderPassDescriptor( - width: Int(renderInfo.viewPort.x), - height: Int(renderInfo.viewPort.y), - colorAttachments: [ - (textureResources.velocityTexture, .clear, .store, MTLClearColorMake(0.0, 0.0, 0.0, 0.0)), - ], - depthAttachment: nil - ) } func initTextureResources() { @@ -479,18 +469,6 @@ func initTextureResources() { storageMode: .private ) - // Opaque-only depth copied after the model passes, before XR transparency can - // write compositor depth. HZB occlusion culling samples this texture. - textureResources.hzbSourceDepthMap = createTexture( - device: renderInfo.device, - label: "HZB Source Depth Texture", - pixelFormat: renderInfo.depthPixelFormat, - width: viewportWidth, - height: viewportHeight, - usage: [.shaderRead], - storageMode: .private - ) - // Deferred Depth Texture textureResources.deferredDepthMap = createTexture( device: renderInfo.device, @@ -645,62 +623,6 @@ func initTextureResources() { storageMode: .shared ) - // TAA: screen-space motion vectors (rg16Float, camera-only velocity). - textureResources.velocityTexture = createTexture( - device: renderInfo.device, - label: "Velocity Texture", - pixelFormat: .rg16Float, - width: viewportWidth, - height: viewportHeight, - usage: [.shaderRead, .renderTarget], - storageMode: .shared - ) - - // TAA output, history, and position-history textures (one set per eye in stereo). - let eyeCount = renderInfo.isXRStereoMode ? 2 : 1 - for eye in 0 ..< eyeCount { - // .shared so the CPU can read it back via getBytes (PSNR tests, debug captures). - // History and position-history remain .private — they are GPU-internal only. - let outTex = createTexture( - device: renderInfo.device, - label: "TAA Output Eye \(eye)", - pixelFormat: wf.lookOutput, - width: viewportWidth, - height: viewportHeight, - usage: [.shaderRead, .renderTarget], - storageMode: .shared - ) - let histTex = createTexture( - device: renderInfo.device, - label: "TAA History Eye \(eye)", - pixelFormat: wf.lookOutput, - width: viewportWidth, - height: viewportHeight, - usage: [.shaderRead, .renderTarget], - storageMode: .private - ) - let posHistTex = createTexture( - device: renderInfo.device, - label: "TAA Position History Eye \(eye)", - pixelFormat: wf.gBufferPosition, - width: viewportWidth, - height: viewportHeight, - usage: [.shaderRead, .renderTarget], - storageMode: .private - ) - - if eye == 0 { - textureResources.taaOutputTexture = outTex - textureResources.taaHistoryTexture = histTex - textureResources.taaPositionHistoryTexture = posHistTex - } - textureResources.taaOutputTextureEye[eye] = outTex - textureResources.taaHistoryTextureEye[eye] = histTex - textureResources.taaPositionHistoryTextureEye[eye] = posHistTex - } - TemporalAA.shared.markReady() - Logger.log(message: "TAA textures initialised (\(eyeCount) eye(s)).") - textureResources.fxaaTexture = createTexture( device: renderInfo.device, label: "FXAA Output Texture", diff --git a/Sources/UntoldEngine/Renderer/RenderPasses.swift b/Sources/UntoldEngine/Renderer/RenderPasses.swift index 68daac9a..3ceeef96 100644 --- a/Sources/UntoldEngine/Renderer/RenderPasses.swift +++ b/Sources/UntoldEngine/Renderer/RenderPasses.swift @@ -377,36 +377,6 @@ public enum RenderPasses { return computed } - public static let copyOpaqueDepthForHZBExecution: RenderPassExecution = { commandBuffer in - guard let sourceDepth = textureResources.depthMap, - let hzbSourceDepth = textureResources.hzbSourceDepthMap - else { - return - } - - let width = min(sourceDepth.width, hzbSourceDepth.width) - let height = min(sourceDepth.height, hzbSourceDepth.height) - guard width > 0, height > 0 else { return } - - guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { - return - } - - blitEncoder.label = "Copy Opaque Depth for HZB" - blitEncoder.copy( - from: sourceDepth, - sourceSlice: 0, - sourceLevel: 0, - sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), - sourceSize: MTLSize(width: width, height: height, depth: 1), - to: hzbSourceDepth, - destinationSlice: 0, - destinationLevel: 0, - destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0) - ) - blitEncoder.endEncoding() - } - @inline(__always) private static func applyLODDebugColorOverride( entityId: EntityID? = nil, @@ -1955,7 +1925,6 @@ public enum RenderPasses { } renderInfo.offscreenRenderPassDescriptor.depthAttachment.loadAction = .load // set the states for the pipeline - renderPassDescriptor.colorAttachments[0].texture = textureResources.deferredColorMap renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadAction.load renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0) renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreAction.store diff --git a/Sources/UntoldEngine/Renderer/RenderResources.swift b/Sources/UntoldEngine/Renderer/RenderResources.swift index dc73c8c0..6e19f31d 100644 --- a/Sources/UntoldEngine/Renderer/RenderResources.swift +++ b/Sources/UntoldEngine/Renderer/RenderResources.swift @@ -60,21 +60,6 @@ public struct RenderInfo { public var isXRStereoMode: Bool = false public var xrEye0ViewProjection: simd_float4x4 = matrix_identity_float4x4 public var xrEye1ViewProjection: simd_float4x4 = matrix_identity_float4x4 - - // TAA / MetalFX Temporal Scaler - // unjitteredPerspectiveSpace is the clean projection matrix used by culling, shadow cascades, - // and velocity computation. perspectiveSpace carries the per-frame Halton jitter for rasterisation. - public var unjitteredPerspectiveSpace: simd_float4x4 = matrix_identity_float4x4 - public var taaJitterX: Float = 0 // current frame jitter offset in pixels (X) - public var taaJitterY: Float = 0 // current frame jitter offset in pixels (Y) - - // Per-eye unjittered view-projection matrices for camera-only velocity computation. - // Index 0 = left / mono, index 1 = right (XR stereo). - public var prevViewProjectionEye: [simd_float4x4] = [matrix_identity_float4x4, matrix_identity_float4x4] - public var currentViewProjectionEye: [simd_float4x4] = [matrix_identity_float4x4, matrix_identity_float4x4] - - /// Velocity pass render pass descriptor (writes rg16Float motion vectors) - public var velocityRenderPassDescriptor: MTLRenderPassDescriptor! } @inline(__always) @@ -172,7 +157,6 @@ public struct TextureResources { public var materialMap: MTLTexture? public var emissiveMap: MTLTexture? public var depthMap: MTLTexture? - public var hzbSourceDepthMap: MTLTexture? public var environmentColorMap: MTLTexture? // deferred @@ -242,21 +226,6 @@ public struct TextureResources { public var depthMapEye: [MTLTexture?] = [nil, nil] public var hzbDepthPyramidEye: [MTLTexture?] = [nil, nil] public var hzbMipViewsEye: [[MTLTexture]] = [[], []] - - // TAA / MetalFX Temporal Scaler textures. - // velocityTexture: rg16Float screen-space motion vectors (camera-only, re-computed each frame). - // taaOutputTexture: resolved TAA output for mono / desktop. - // taaOutputTextureEye[]: per-eye resolved TAA output for XR stereo. - public var velocityTexture: MTLTexture? - public var taaOutputTexture: MTLTexture? - public var taaOutputTextureEye: [MTLTexture?] = [nil, nil] - // History textures: the previous frame's resolved TAA output, read by the resolve shader. - public var taaHistoryTexture: MTLTexture? - public var taaHistoryTextureEye: [MTLTexture?] = [nil, nil] - // Previous frame's world-position G-buffer, used to reject stale history on - // disocclusions and object motion that camera-only velocity cannot represent. - public var taaPositionHistoryTexture: MTLTexture? - public var taaPositionHistoryTextureEye: [MTLTexture?] = [nil, nil] } public struct AccelStructResources { diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 7bc2d8fd..d31a8f89 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -502,40 +502,6 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { initSizeableResources() pendingResize = false } - - // TAA jitter (desktop / mono path). - // Apply a per-frame Halton sub-pixel offset to the projection matrix so the - // temporal scaler can accumulate multiple sample positions over time. - // Culling and shadow systems use unjitteredPerspectiveSpace to stay stable. - if TAAParams.shared.enabled, TemporalAA.shared.isSupported { - let jitter = TemporalAA.shared.currentJitter() - let vw = Float(renderInfo.viewPort.x) - let vh = Float(renderInfo.viewPort.y) - var jittered = renderInfo.unjitteredPerspectiveSpace - jittered[3][0] += jitter.x * 2.0 / vw - jittered[3][1] += jitter.y * 2.0 / vh - renderInfo.perspectiveSpace = jittered - renderInfo.taaJitterX = jitter.x - renderInfo.taaJitterY = jitter.y - - // Track prev/current unjittered VP for the camera-only velocity pass. - if let cam = CameraSystem.shared.activeCamera, - let camComp = scene.get(component: CameraComponent.self, for: cam) - { - let effectiveView = SceneRootTransform.shared.effectiveViewMatrix(camComp.viewSpace) - let unjitteredVP = simd_mul(renderInfo.unjitteredPerspectiveSpace, effectiveView) - renderInfo.prevViewProjectionEye[0] = renderInfo.currentViewProjectionEye[0] - renderInfo.currentViewProjectionEye[0] = unjitteredVP - - // Auto-reset TAA history after large camera jumps (teleport / scene cut). - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: camComp.localPosition) - } - } else { - renderInfo.perspectiveSpace = renderInfo.unjitteredPerspectiveSpace - renderInfo.taaJitterX = 0 - renderInfo.taaJitterY = 0 - } - // Tick the progressive loader here (main thread, before runFrame) so newly // registered entities are picked up by BatchingSystem in the same frame. // In XR, UntoldEngineXR.renderNewFrame() dispatches this to the main thread @@ -563,9 +529,6 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { fovyRadians: degreesToRadians(degrees: fov), aspectRatio: aspect, nearZ: near, farZ: far ) - // Store the clean (unjittered) projection. The per-frame draw() applies Halton - // jitter on top of this into perspectiveSpace before each render. - renderInfo.unjitteredPerspectiveSpace = projectionMatrix renderInfo.perspectiveSpace = projectionMatrix let viewPortSize: simd_float2 = simd_make_float2(Float(size.width), Float(size.height)) @@ -613,12 +576,6 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { renderer.initResources() - // On Vision Pro the display resolution is high enough that spatial aliasing is - // barely visible, and TAA's temporal ghosting is more noticeable than aliasing - // on a precision head-tracked display. FXAA gives clean edges with zero lag. - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = true - renderEnvironment = true return renderer @@ -675,6 +632,8 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { projectionMatrix: simd_float4x4, eyeIndex: Int ) { + renderInfo.perspectiveSpace = projectionMatrix + guard let camera = CameraSystem.shared.activeCamera, let cameraComponent = scene.get(component: CameraComponent.self, for: camera) else { handleError(.noActiveCamera) return @@ -682,45 +641,12 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { cameraComponent.viewSpace = viewMatrix - // --- Unjittered VP (used by culling, shadow cascades, and velocity) --- - let effectiveVM = SceneRootTransform.shared.effectiveViewMatrix(viewMatrix) - let unjitteredVP = simd_mul(projectionMatrix, effectiveVM) - - // Save this eye's unjittered VP for next frame's per-eye HZB culling. + // Save this eye's view-projection for next frame's per-eye HZB culling. if renderInfo.isXRStereoMode { - if eyeIndex == 0 { renderInfo.xrEye0ViewProjection = unjitteredVP } - else { renderInfo.xrEye1ViewProjection = unjitteredVP } - } - - // Track prev/current unjittered VP for the camera-only velocity pass. - let safeEye = min(eyeIndex, 1) - renderInfo.prevViewProjectionEye[safeEye] = renderInfo.currentViewProjectionEye[safeEye] - renderInfo.currentViewProjectionEye[safeEye] = unjitteredVP - - // --- Jittered projection (rasterisation only) --- - // Both eyes share the same Halton sample for a given frame so the temporal - // accumulation in each per-eye scaler sees a consistent sample pattern. - if TAAParams.shared.enabled, TemporalAA.shared.isSupported { - let jitter = TemporalAA.shared.currentJitter() - let vw = Float(renderInfo.viewPort.x) - let vh = Float(renderInfo.viewPort.y) - var jittered = projectionMatrix - jittered[3][0] += jitter.x * 2.0 / vw - jittered[3][1] += jitter.y * 2.0 / vh - renderInfo.perspectiveSpace = jittered - renderInfo.unjitteredPerspectiveSpace = projectionMatrix - renderInfo.taaJitterX = jitter.x - renderInfo.taaJitterY = jitter.y - - // Auto-reset TAA history after large head-position jumps (XR teleport). - if safeEye == 0 { - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: cameraComponent.localPosition) - } - } else { - renderInfo.perspectiveSpace = projectionMatrix - renderInfo.unjitteredPerspectiveSpace = projectionMatrix - renderInfo.taaJitterX = 0 - renderInfo.taaJitterY = 0 + let effectiveVM = SceneRootTransform.shared.effectiveViewMatrix(viewMatrix) + let eyeVP = simd_mul(projectionMatrix, effectiveVM) + if eyeIndex == 0 { renderInfo.xrEye0ViewProjection = eyeVP } + else { renderInfo.xrEye1ViewProjection = eyeVP } } configuration.updateXRRenderingSystemCallback!(.xr(commandBuffer: commandBuffer, passDescriptor: passDescriptor)) diff --git a/Sources/UntoldEngine/Shaders/taaResolveShader.metal b/Sources/UntoldEngine/Shaders/taaResolveShader.metal deleted file mode 100644 index 3731fbf0..00000000 --- a/Sources/UntoldEngine/Shaders/taaResolveShader.metal +++ /dev/null @@ -1,149 +0,0 @@ -// -// taaResolveShader.metal -// Untold Engine -// -// Copyright (C) Untold Engine Studios -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Custom Temporal Anti-Aliasing resolve pass. -// -// Algorithm (neighbourhood-clamped temporal accumulation): -// 1. Sample current-frame colour and reprojected history (via motion vectors). -// 2. Build a tight AABB from a 3×3 neighbourhood in the current frame. -// 3. Clamp history to that AABB — kills ghosting when objects move. -// 4. Blend adaptively: base weight (0.65 XR / 0.1 desktop) is floored up by -// two signals — per-pixel motion and camera-centre displacement — so that -// head rotation clears history quickly even for pixels at the rotation axis. -// -// On the first frame (reset == true) the shader outputs current colour only so -// no garbage history bleeds into the accumulated result. - -#include -#include "../../CShaderTypes/ShaderTypes.h" -#include "ShaderStructs.h" -using namespace metal; - -typedef enum { - taaCurrentColorIndex = 0, - taaHistoryColorIndex = 1, - taaVelocityMapIndex = 2, - taaPositionMapIndex = 3, - taaPositionHistoryIndex = 4, -} TAATextureIndices; - -typedef enum { - taaViewportSizeIndex = 0, - taaResetFlagIndex = 1, - taaBlendFactorIndex = 2, - taaCamDisplacementIndex = 3, - taaCameraPositionIndex = 4, - taaPositionRejectBaseIndex = 5, - taaPositionRejectDistanceScaleIndex = 6, - taaMotionBlendStartIndex = 7, - taaMotionBlendEndIndex = 8, - taaCameraBoostStartIndex = 9, - taaCameraBoostEndIndex = 10, - taaClampRadiusIndex = 11, -} TAABufferIndices; - -vertex VertexCompositeOutput vertexTAAResolveShader(VertexCompositeIn in [[stage_in]]) { - VertexCompositeOutput out; - out.position = float4(float3(in.position), 1.0); - out.uvCoords = in.uvCoords; - return out; -} - -fragment float4 fragmentTAAResolveShader( - VertexCompositeOutput in [[stage_in]], - texture2d currentColor [[texture(taaCurrentColorIndex)]], - texture2d historyColor [[texture(taaHistoryColorIndex)]], - texture2d velocityMap [[texture(taaVelocityMapIndex)]], - texture2d positionMap [[texture(taaPositionMapIndex)]], - texture2d positionHistory [[texture(taaPositionHistoryIndex)]], - constant float2 &viewportSize [[buffer(taaViewportSizeIndex)]], - constant uint &resetFlag [[buffer(taaResetFlagIndex)]], - constant float &blendFactor [[buffer(taaBlendFactorIndex)]], - constant float &camDisplacement [[buffer(taaCamDisplacementIndex)]], - constant float3 &cameraPosition [[buffer(taaCameraPositionIndex)]], - constant float &positionRejectBase [[buffer(taaPositionRejectBaseIndex)]], - constant float &positionRejectDistanceScale [[buffer(taaPositionRejectDistanceScaleIndex)]], - constant float &motionBlendStart [[buffer(taaMotionBlendStartIndex)]], - constant float &motionBlendEnd [[buffer(taaMotionBlendEndIndex)]], - constant float &cameraBoostStart [[buffer(taaCameraBoostStartIndex)]], - constant float &cameraBoostEnd [[buffer(taaCameraBoostEndIndex)]], - constant int &clampRadiusParam [[buffer(taaClampRadiusIndex)]] -) { - uint2 gid = uint2(in.position.xy); - float4 current = currentColor.read(gid); - - // First frame or after a scene cut — skip history to avoid garbage blend. - if (resetFlag != 0u) return current; - - // Pixels without opaque G-buffer position do not have reliable camera-only - // velocity, so do not blend old history into them. - float4 worldPos = positionMap.read(gid); - if (worldPos.w < 0.5) return current; - - // Reproject: find the previous-frame UV for this pixel using motion vectors. - // velocityMap stores pixel-space backward motion (current -> previous). - float2 motion = float2(velocityMap.read(gid)); - float2 prevUV = (float2(gid) + motion + 0.5) / viewportSize; - - // Disocclusion / offscreen history rejection. Clamping here would pull old - // edge pixels into the current frame during fast head movement. - if (any(prevUV < 0.0) || any(prevUV > 1.0)) return current; - - constexpr sampler bilinear(coord::normalized, filter::linear, address::clamp_to_edge); - - float4 previousWorldPos = positionHistory.sample(bilinear, prevUV); - if (previousWorldPos.w < 0.5) return current; - - // Camera/head motion should reproject to the same world-space surface. - // Reject history when it does not; this catches disocclusion and stale - // reprojected samples around silhouettes. - float positionDelta = length(previousWorldPos.xyz - worldPos.xyz); - float cameraDistance = length(worldPos.xyz - cameraPosition); - float positionRejectThreshold = max(0.0, positionRejectBase) - + cameraDistance * max(0.0, positionRejectDistanceScale); - if (positionDelta > positionRejectThreshold) return current; - - float4 history = historyColor.sample(bilinear, prevUV); - - // Build a colour AABB in the current frame to clamp the history. - // This is the key ghost-rejection step: history that falls outside the - // neighbourhood's plausible colour range is pulled back to the boundary. - int2 iSize = int2(viewportSize); - int radius = clamp(clampRadiusParam, 1, 2); - float4 minC = current, maxC = current; - for (int dy = -radius; dy <= radius; dy++) { - for (int dx = -radius; dx <= radius; dx++) { - if (dx == 0 && dy == 0) continue; - int2 coord = clamp(int2(gid) + int2(dx, dy), int2(0), iSize - 1); - float4 nb = currentColor.read(uint2(coord)); - minC = min(minC, nb); - maxC = max(maxC, nb); - } - } - history = clamp(history, minC, maxC); - - // Temporal blend. Two signals drive the current-frame weight: - // - // 1. Per-pixel motion magnitude — handles edge/object motion. - // 2. Camera displacement (passed from CPU) — handles head rotation where - // center pixels have near-zero local motion but the whole view has changed. - // smoothstep(2, 20) ramps the boost from zero at 2 px to full at 20 px of - // view-center displacement, raising the floor blend for all pixels so that - // even the rotation-axis center converges quickly after a head turn. - float motionPixels = length(motion); - float motionStart = min(motionBlendStart, motionBlendEnd); - float motionEnd = max(max(motionBlendStart, motionBlendEnd), motionStart + 0.001); - float cameraStart = min(cameraBoostStart, cameraBoostEnd); - float cameraEnd = max(max(cameraBoostStart, cameraBoostEnd), cameraStart + 0.001); - float cameraBoost = smoothstep(cameraStart, cameraEnd, camDisplacement); - float adaptiveBlend = max(blendFactor + cameraBoost * (1.0 - blendFactor), - smoothstep(motionStart, motionEnd, motionPixels)); - return mix(history, current, adaptiveBlend); -} diff --git a/Sources/UntoldEngine/Shaders/velocityShader.metal b/Sources/UntoldEngine/Shaders/velocityShader.metal deleted file mode 100644 index 6a23750a..00000000 --- a/Sources/UntoldEngine/Shaders/velocityShader.metal +++ /dev/null @@ -1,73 +0,0 @@ -// -// velocityShader.metal -// Untold Engine -// -// Copyright (C) Untold Engine Studios -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Camera-only velocity (motion vector) pass for TAA / MetalFX Temporal Scaler. -// -// For every pixel this shader reconstructs the world-space position from the -// G-buffer positionMap, projects it with the current and previous (unjittered) -// view-projection matrices, then outputs the pixel-space delta as rg16Float. -// -// Motion vector convention (matches MetalFX default with scaleX/Y = 1): -// +x = moved right (screen-space) -// +y = moved DOWN (screen-space / Metal texture origin) - -#include -#include "../../CShaderTypes/ShaderTypes.h" -#include "ShaderStructs.h" -using namespace metal; - -typedef enum { - velocityPositionMapIndex = 0, -} VelocityTextureIndices; - -typedef enum { - velocityCurrentVPIndex = 0, - velocityPreviousVPIndex = 1, - velocityViewportIndex = 2, -} VelocityBufferIndices; - -vertex VertexCompositeOutput vertexVelocityShader(VertexCompositeIn in [[stage_in]]) { - VertexCompositeOutput out; - out.position = float4(float3(in.position), 1.0); - out.uvCoords = in.uvCoords; - return out; -} - -fragment half2 fragmentVelocityShader( - VertexCompositeOutput in [[stage_in]], - texture2d positionMap [[texture(velocityPositionMapIndex)]], - constant float4x4 ¤tVP [[buffer(velocityCurrentVPIndex)]], - constant float4x4 &previousVP [[buffer(velocityPreviousVPIndex)]], - constant float2 &viewportSize [[buffer(velocityViewportIndex)]] -) { - // Read the world-space position stored by the G-buffer model pass. - // w == 0 means background (sky / no geometry) → zero velocity. - uint2 gid = uint2(in.position.xy); - float4 worldPos = positionMap.read(gid); - if (worldPos.w < 0.5h) { - return half2(0.0h); - } - - // Project world position with unjittered current and previous VP matrices. - float4 clipCurrent = currentVP * float4(worldPos.xyz, 1.0); - float4 clipPrev = previousVP * float4(worldPos.xyz, 1.0); - - float2 ndcCurrent = clipCurrent.xy / clipCurrent.w; - float2 ndcPrev = clipPrev.xy / clipPrev.w; - - // Motion vectors point from the current pixel BACK to where it came from in the - // previous frame (reprojection direction), which is what MetalFX expects. - // NDC +Y is up; Metal texture +Y is down, so the Y sign flips cancel out: - // backward direction negate (-ndcDelta) × screen-Y-flip negate = no net Y negate. - float2 ndcDelta = ndcCurrent - ndcPrev; - float2 pixelMotion = float2(-ndcDelta.x, ndcDelta.y) * viewportSize * 0.5; - - return half2(pixelMotion); -} diff --git a/Sources/UntoldEngine/Systems/CullingSystem.swift b/Sources/UntoldEngine/Systems/CullingSystem.swift index b00bfda1..643d36ad 100644 --- a/Sources/UntoldEngine/Systems/CullingSystem.swift +++ b/Sources/UntoldEngine/Systems/CullingSystem.swift @@ -387,7 +387,7 @@ public func buildHZBDepthPyramid(_ commandBuffer: MTLCommandBuffer, eyeIndex: In return } - guard let depthTexture = textureResources.hzbSourceDepthMap ?? textureResources.depthMap else { + guard let depthTexture = textureResources.depthMap else { handleError(.textureMissing, "Depth Texture") renderInfo.hzbIsValid = false textureResources.hzbDebugMipTexture = nil diff --git a/Sources/UntoldEngine/Systems/RenderingSystem.swift b/Sources/UntoldEngine/Systems/RenderingSystem.swift index 50e73ae7..f91a1837 100644 --- a/Sources/UntoldEngine/Systems/RenderingSystem.swift +++ b/Sources/UntoldEngine/Systems/RenderingSystem.swift @@ -55,12 +55,7 @@ func UpdateRenderingSystem(in view: MTKView) { let cullingStart = CACurrentMediaTime() #endif EngineProfiler.shared.beginScope(.culling) - // Culling must see the unjittered projection so frustum planes are stable. - // The render graph (executeGraph below) still gets the jittered perspectiveSpace. - let jitteredProj = renderInfo.perspectiveSpace - renderInfo.perspectiveSpace = renderInfo.unjitteredPerspectiveSpace performFrustumCulling(commandBuffer: commandBuffer) - renderInfo.perspectiveSpace = jitteredProj EngineProfiler.shared.endScope(.culling) #if ENGINE_STATS_ENABLED let cullingMs = (CACurrentMediaTime() - cullingStart) * 1000.0 @@ -292,52 +287,11 @@ public func buildGameModeGraph() -> RenderGraphResult { let gaussianPass = RenderPass(id: "gaussian", dependencies: ["model"], execute: RenderPasses.gaussianExecution) graph[gaussianPass.id] = gaussianPass - // TAA and FXAA are mutually exclusive. TAA takes priority. - let taaEnabled = TAAParams.shared.enabled && TemporalAA.shared.isSupported - let postProcessInputID: String - - if taaEnabled { - // Camera-only velocity pass: reads positionMap (written by batchedModel) and outputs - // pixel-space motion vectors for temporal reprojection. - let velocityPass = RenderPass( - id: "velocity", - dependencies: ["batchedModel"], - execute: velocityRenderPass - ) - graph[velocityPass.id] = velocityPass - - // TAA resolves the lit scene before post-processing so effects like bloom, - // DoF, vignette, and the final look pass do not get temporally smeared. - let taaPass = RenderPass( - id: "taa", - dependencies: [spatialDebugPass.id, velocityPass.id], - execute: taaRenderPass - ) - graph[taaPass.id] = taaPass - - let taaPostProcessSourcePass = RenderPass( - id: "taaPostProcessSource", - dependencies: [taaPass.id], - execute: { _ in - let eyeIdx = renderInfo.isXRStereoMode ? renderInfo.currentEye : 0 - let safeEye = min(eyeIdx, 1) - let resolvedTexture = safeEye == 0 - ? textureResources.taaOutputTexture - : textureResources.taaOutputTextureEye[safeEye] - renderInfo.deferredRenderPassDescriptor?.colorAttachments[0].texture = resolvedTexture - } - ) - graph[taaPostProcessSourcePass.id] = taaPostProcessSourcePass - postProcessInputID = taaPostProcessSourcePass.id - } else { - postProcessInputID = spatialDebugPass.id - } - let postProcessID: String if bypassPostProcessing { let bypassPass = RenderPass( id: "postProcessBypass", - dependencies: [postProcessInputID], + dependencies: [spatialDebugPass.id], execute: { _ in guard let deferredDescriptor = renderInfo.deferredRenderPassDescriptor else { return @@ -349,7 +303,7 @@ public func buildGameModeGraph() -> RenderGraphResult { graph[bypassPass.id] = bypassPass postProcessID = bypassPass.id } else { - let postProcess = postProcessingEffects(graph: &graph, deferredPassId: postProcessInputID) + let postProcess = postProcessingEffects(graph: &graph, deferredPassId: spatialDebugPass.id) postProcessID = postProcess.id } @@ -369,8 +323,7 @@ public func buildGameModeGraph() -> RenderGraphResult { graph[lookPass.id] = lookPass let outputDependency: String - - if !taaEnabled, FXAAParams.shared.enabled { + if FXAAParams.shared.enabled { let fxaaPass = RenderPass( id: "fxaa", dependencies: [lookPass.id], @@ -403,12 +356,6 @@ func gBufferPass(graph: inout [String: RenderPass], shadowPass: RenderPass) { id: "batchedModel", dependencies: [modelPass.id], execute: RenderPasses.batchedModelExecution ) graph[batchedModelPass.id] = batchedModelPass - let hzbDepthSourcePass = RenderPass( - id: "hzbDepthSource", - dependencies: [batchedModelPass.id], - execute: RenderPasses.copyOpaqueDepthForHZBExecution - ) - graph[hzbDepthSourcePass.id] = hzbDepthSourcePass // Update SSAO to depend on batched pass let ssaoPass = RenderPass( id: "ssao", @@ -421,7 +368,7 @@ func gBufferPass(graph: inout [String: RenderPass], shadowPass: RenderPass) { // Note: ssaoOptimizedExecution handles all blur/upsample internally // No need for separate ssaoBlur pass in the graph - let lightPass = RenderPass(id: "lightPass", dependencies: [hzbDepthSourcePass.id, modelPass.id, shadowPass.id, ssaoPass.id], execute: RenderPasses.lightExecution) + let lightPass = RenderPass(id: "lightPass", dependencies: [batchedModelPass.id, modelPass.id, shadowPass.id, ssaoPass.id], execute: RenderPasses.lightExecution) graph[lightPass.id] = lightPass } @@ -788,7 +735,7 @@ func chromaticAberrationCustomization(encoder: MTLRenderCommandEncoder) { } let depthOfFieldRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in - guard let sourceTexture = renderInfo.deferredRenderPassDescriptor?.colorAttachments[0].texture, + guard let sourceTexture = textureResources.deferredColorMap, let destinationTexture = textureResources.depthOfFieldTexture, let pipeline = PipelineManager.shared.renderPipelinesByType[.depthOfField] else { @@ -836,198 +783,6 @@ func depthOfFieldCustomization(encoder: MTLRenderCommandEncoder) { encoder.setFragmentBytes(&reverseZ, length: MemoryLayout.stride, index: Int(depthOfFieldPassReverseZIndex.rawValue)) } -// MARK: - Camera-only Velocity Pass - -public let velocityRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in - guard TAAParams.shared.enabled && TemporalAA.shared.isSupported else { return } - - guard let pipeline = PipelineManager.shared.renderPipelinesByType[.velocity] else { - handleError(.pipelineStateNulled, "Velocity Pipeline is nil") - return - } - guard let descriptor = renderInfo.velocityRenderPassDescriptor else { return } - - // Keep the velocity descriptor pointing at the current velocity texture - // (it may have been reallocated after a viewport resize). - descriptor.colorAttachments[0].texture = textureResources.velocityTexture - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { - handleError(.renderPassCreationFailed, "Velocity Pass") - return - } - renderEncoder.label = "Velocity Pass" - renderEncoder.pushDebugGroup("Velocity Pass") - defer { renderEncoder.popDebugGroup(); renderEncoder.endEncoding() } - - renderEncoder.setRenderPipelineState(pipeline.pipelineState!) - renderEncoder.setVertexBuffer(bufferResources.quadVerticesBuffer, offset: 0, index: 0) - renderEncoder.setVertexBuffer(bufferResources.quadTexCoordsBuffer, offset: 0, index: 1) - - renderEncoder.setFragmentTexture(textureResources.positionMap, index: 0) - - // Determine which eye's VP matrices to use. - let eyeIdx = renderInfo.isXRStereoMode ? renderInfo.currentEye : 0 - let safeEye = min(eyeIdx, 1) - var currentVP = renderInfo.currentViewProjectionEye[safeEye] - var previousVP = renderInfo.prevViewProjectionEye[safeEye] - var viewport = renderInfo.viewPort ?? simd_float2(1, 1) - - renderEncoder.setFragmentBytes(¤tVP, length: MemoryLayout.stride, index: 0) - renderEncoder.setFragmentBytes(&previousVP, length: MemoryLayout.stride, index: 1) - renderEncoder.setFragmentBytes(&viewport, length: MemoryLayout.stride, index: 2) - - renderEncoder.drawIndexedPrimitivesTracked( - type: .triangle, indexCount: 6, indexType: .uint16, - indexBuffer: bufferResources.quadIndexBuffer!, - indexBufferOffset: 0 - ) -} - -// MARK: - Custom TAA Resolve Pass - -public let taaRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in - guard TAAParams.shared.enabled && TemporalAA.shared.isSupported else { return } - guard let pipeline = PipelineManager.shared.renderPipelinesByType[.taaResolve] else { - handleError(.pipelineStateNulled, "TAA Resolve Pipeline is nil") - return - } - - let eyeIdx = renderInfo.isXRStereoMode ? renderInfo.currentEye : 0 - let safeEye = min(eyeIdx, 1) - - guard - let currentColor = textureResources.deferredColorMap, - let velocityTex = textureResources.velocityTexture, - let positionTex = textureResources.positionMap, - let outputTex = (safeEye == 0 - ? textureResources.taaOutputTexture - : textureResources.taaOutputTextureEye[safeEye]), - let historyTex = (safeEye == 0 - ? textureResources.taaHistoryTexture - : textureResources.taaHistoryTextureEye[safeEye]), - let positionHistoryTex = (safeEye == 0 - ? textureResources.taaPositionHistoryTexture - : textureResources.taaPositionHistoryTextureEye[safeEye]) - else { - handleError(.renderPassCreationFailed, "TAA Resolve: missing textures for eye \(safeEye)") - return - } - - // --- Resolve pass --- - let descriptor = MTLRenderPassDescriptor() - descriptor.colorAttachments[0].texture = outputTex - descriptor.colorAttachments[0].loadAction = .dontCare - descriptor.colorAttachments[0].storeAction = .store - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { - handleError(.renderPassCreationFailed, "TAA Resolve encoder for eye \(safeEye)") - return - } - renderEncoder.label = "TAA Resolve Eye \(safeEye)" - renderEncoder.pushDebugGroup("TAA Resolve") - - renderEncoder.setRenderPipelineState(pipeline.pipelineState!) - renderEncoder.setVertexBuffer(bufferResources.quadVerticesBuffer, offset: 0, index: 0) - renderEncoder.setVertexBuffer(bufferResources.quadTexCoordsBuffer, offset: 0, index: 1) - - renderEncoder.setFragmentTexture(currentColor, index: 0) - renderEncoder.setFragmentTexture(historyTex, index: 1) - renderEncoder.setFragmentTexture(velocityTex, index: 2) - renderEncoder.setFragmentTexture(positionTex, index: 3) - renderEncoder.setFragmentTexture(positionHistoryTex, index: 4) - - var viewport = renderInfo.viewPort ?? simd_float2(1, 1) - var resetFlag = UInt32(TemporalAA.shared.needsReset ? 1 : 0) - // XR uses a higher base weight for the current frame so the history sheds - // faster after head movement (0.65 → 35% history vs 0.1 → 90% for desktop). - let taaParams = TAAParams.shared - var blendFactor = renderInfo.isXRStereoMode - ? min(max(taaParams.xrBlendFactor, 0.0), 1.0) - : min(max(taaParams.desktopBlendFactor, 0.0), 1.0) - - // Compute camera centre displacement in pixels between this frame and the - // previous one. Projecting a fixed reference point through both unjittered - // VP matrices gives a single scalar that reflects how much the whole view - // has shifted — including rotation where per-pixel motion at the image - // centre is near zero. - let curVP = renderInfo.currentViewProjectionEye[safeEye] - let prevVP = renderInfo.prevViewProjectionEye[safeEye] - let ref = simd_float4(0, 0, -1, 1) - let clipC = simd_mul(curVP, ref) - let clipP = simd_mul(prevVP, ref) - var camDisplacementPixels: Float = 0.0 - if clipC.w > 0.001 && clipP.w > 0.001 { - let ndcC = simd_float2(clipC.x / clipC.w, clipC.y / clipC.w) - let ndcP = simd_float2(clipP.x / clipP.w, clipP.y / clipP.w) - camDisplacementPixels = simd_length((ndcC - ndcP) * viewport * 0.5) - } - - var cameraPosition = simd_float3.zero - if let camera = CameraSystem.shared.activeCamera, - let cameraComponent = scene.get(component: CameraComponent.self, for: camera) - { - cameraPosition = SceneRootTransform.shared.effectiveCameraPosition(cameraComponent.localPosition) - } - var positionRejectBase = taaParams.positionRejectBase - var positionRejectDistanceScale = taaParams.positionRejectDistanceScale - var motionBlendStart = taaParams.motionBlendStartPixels - var motionBlendEnd = taaParams.motionBlendEndPixels - var cameraBoostStart = taaParams.cameraBoostStartPixels - var cameraBoostEnd = taaParams.cameraBoostEndPixels - var clampRadius = taaParams.clampNeighborhoodRadius - - renderEncoder.setFragmentBytes(&viewport, length: MemoryLayout.stride, index: 0) - renderEncoder.setFragmentBytes(&resetFlag, length: MemoryLayout.stride, index: 1) - renderEncoder.setFragmentBytes(&blendFactor, length: MemoryLayout.stride, index: 2) - renderEncoder.setFragmentBytes(&camDisplacementPixels, length: MemoryLayout.stride, index: 3) - renderEncoder.setFragmentBytes(&cameraPosition, length: MemoryLayout.stride, index: 4) - renderEncoder.setFragmentBytes(&positionRejectBase, length: MemoryLayout.stride, index: 5) - renderEncoder.setFragmentBytes(&positionRejectDistanceScale, length: MemoryLayout.stride, index: 6) - renderEncoder.setFragmentBytes(&motionBlendStart, length: MemoryLayout.stride, index: 7) - renderEncoder.setFragmentBytes(&motionBlendEnd, length: MemoryLayout.stride, index: 8) - renderEncoder.setFragmentBytes(&cameraBoostStart, length: MemoryLayout.stride, index: 9) - renderEncoder.setFragmentBytes(&cameraBoostEnd, length: MemoryLayout.stride, index: 10) - renderEncoder.setFragmentBytes(&clampRadius, length: MemoryLayout.stride, index: 11) - - renderEncoder.drawIndexedPrimitivesTracked( - type: .triangle, indexCount: 6, indexType: .uint16, - indexBuffer: bufferResources.quadIndexBuffer!, - indexBufferOffset: 0 - ) - renderEncoder.popDebugGroup() - renderEncoder.endEncoding() - - // --- History blit: copy resolved output → history for next frame --- - if let blitEncoder = commandBuffer.makeBlitCommandEncoder() { - blitEncoder.label = "TAA History Blit Eye \(safeEye)" - blitEncoder.copy( - from: outputTex, - sourceSlice: 0, sourceLevel: 0, - sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), - sourceSize: MTLSize(width: outputTex.width, height: outputTex.height, depth: 1), - to: historyTex, - destinationSlice: 0, destinationLevel: 0, - destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0) - ) - blitEncoder.copy( - from: positionTex, - sourceSlice: 0, sourceLevel: 0, - sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), - sourceSize: MTLSize(width: positionTex.width, height: positionTex.height, depth: 1), - to: positionHistoryTex, - destinationSlice: 0, destinationLevel: 0, - destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0) - ) - blitEncoder.endEncoding() - } - - // Advance Halton counter once after the last eye. - let isLastEye = !renderInfo.isXRStereoMode || renderInfo.currentEye == 1 - if isLastEye { TemporalAA.shared.advanceFrame() } -} - -// MARK: - FXAA Pass - public let fxaaRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in guard let sourceTexture = textureResources.lookTexture, let destinationTexture = textureResources.fxaaTexture, @@ -1166,12 +921,9 @@ public let lookRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in } public let outputTransformRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in - let sourceTexture: MTLTexture? - if FXAAParams.shared.enabled, !(TAAParams.shared.enabled && TemporalAA.shared.isSupported) { - sourceTexture = textureResources.fxaaTexture - } else { - sourceTexture = textureResources.lookTexture - } + let sourceTexture = FXAAParams.shared.enabled + ? textureResources.fxaaTexture + : textureResources.lookTexture guard let sourceTexture else { handleError(.renderPassCreationFailed, "Output Transform Pass: source texture is nil") return diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air index 25eecfe4..df07c23a 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib index f6b22e31..e7634169 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air index c2679853..b1a09602 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib index 61f7dab0..300cafb2 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air index f8ed0e07..e9ba3b45 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib index caa58fe9..a114b57d 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air index 64d34638..2866d7fa 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib index ae404ab5..a34d7efd 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air index c0002cb1..26ef7b18 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib index 17d3a98c..a8892d1e 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metal b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metal index 98c62e3a..b2bf1192 100644 --- a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metal +++ b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metal @@ -43,9 +43,6 @@ using namespace metal; #include "../Shaders/SSAOUpsampleShader.metal" #include "../Shaders/spatialDebugShader.metal" #include "../Shaders/FXAAShader.metal" -#include "../Shaders/velocityShader.metal" -#include "../Shaders/taaResolveShader.metal" - // Gaussian kernels #include "../Shaders/BitonicSort.metal" #include "../Shaders/Gaussians.metal" diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib index b877e35e..c55153bc 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib differ diff --git a/Sources/UntoldEngine/Utils/FuncUtils.swift b/Sources/UntoldEngine/Utils/FuncUtils.swift index 963394c9..0aa7330e 100644 --- a/Sources/UntoldEngine/Utils/FuncUtils.swift +++ b/Sources/UntoldEngine/Utils/FuncUtils.swift @@ -1408,18 +1408,3 @@ public func enableBatching(_ enabled: Bool) { public func isBatchingEnabled() -> Bool { BatchingSystem.shared.isEnabled() } - -// MARK: - Halton low-discrepancy sequence - -private func halton(_ index: Int, _ base: Int) -> Float { - var f: Float = 1; var r: Float = 0; var i = index - while i > 0 { - f /= Float(base); r += f * Float(i % base); i /= base - } - return r -} - -/// Pre-computed Halton(2,3) sub-pixel jitter table (16 samples, range [-0.5, 0.5]). -/// Consumed by TemporalAA.currentJitter() for per-frame projection jitter. -let haltonJitterTable: [simd_float2] = - (1 ... 16).map { i in simd_float2(halton(i, 2) - 0.5, halton(i, 3) - 0.5) } diff --git a/Sources/UntoldEngine/Utils/Globals.swift b/Sources/UntoldEngine/Utils/Globals.swift index 0e19c824..bdb08f83 100644 --- a/Sources/UntoldEngine/Utils/Globals.swift +++ b/Sources/UntoldEngine/Utils/Globals.swift @@ -259,9 +259,6 @@ let shadowMaxHeight: Float = 300.0 let csmCascadeCount: Int = 3 let shadowResolution: simd_int2 = .init(2048, 2048) -// TAA: Halton table size (frame counter lives inside TemporalAA) -let haltonTableSize: Int = 16 - var rayTracingPipeline: ComputePipeline { get { let state = CoreRuntimeGlobals.shared @@ -1476,41 +1473,12 @@ public final class DepthOfFieldParams: ObservableObject, @unchecked Sendable { public final class FXAAParams: ObservableObject, @unchecked Sendable { public static let shared = FXAAParams() - @Published public var enabled: Bool = false + @Published public var enabled: Bool = true @Published public var subpixelQuality: Float = 0.75 // 0.0–1.0; higher = stronger sub-pixel smoothing @Published public var edgeThreshold: Float = 0.125 // minimum local contrast to trigger AA @Published public var edgeThresholdMin: Float = 0.0625 // absolute threshold floor (skip very dark edges) } -/// TAA + MetalFX Temporal Scaler parameters. -/// TAA and FXAA are mutually exclusive — enabling TAA auto-disables FXAA. -public final class TAAParams: ObservableObject, @unchecked Sendable { - public static let shared = TAAParams() - - @Published public var enabled: Bool = true - - /// Current-frame blend weight when running on the desktop / mono path. - @Published public var desktopBlendFactor: Float = 0.1 - - /// Current-frame blend weight when running stereo XR. - @Published public var xrBlendFactor: Float = 0.65 - - /// Pixel-motion range that ramps the resolve from history-heavy to current-heavy. - @Published public var motionBlendStartPixels: Float = 0.5 - @Published public var motionBlendEndPixels: Float = 8.0 - - /// Camera/view-center displacement range that increases current-frame weight. - @Published public var cameraBoostStartPixels: Float = 2.0 - @Published public var cameraBoostEndPixels: Float = 20.0 - - /// Base world-space rejection threshold plus distance-scaled slack. - @Published public var positionRejectBase: Float = 0.03 - @Published public var positionRejectDistanceScale: Float = 0.001 - - /// Radius for current-frame color clamp neighbourhood. 1 = 3x3, 2 = 5x5. - @Published public var clampNeighborhoodRadius: Int32 = 1 -} - /// SSAO Quality Settings public enum SSAOQuality: Int, CaseIterable { case fast = 0 diff --git a/Sources/UntoldEngine/Utils/TemporalAA.swift b/Sources/UntoldEngine/Utils/TemporalAA.swift deleted file mode 100644 index d61f26cf..00000000 --- a/Sources/UntoldEngine/Utils/TemporalAA.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// TemporalAA.swift -// Untold Engine -// -// Copyright (C) Untold Engine Studios -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import Foundation -import simd - -/// Lifecycle and per-frame state for the Temporal Anti-Aliasing resolve pass. -/// Texture creation lives in initTextureResources (RenderInitializer.swift). -/// The Halton jitter table lives in FuncUtils.swift. -/// GPU encoding is handled by taaRenderPass in RenderingSystem.swift. -final class TemporalAA: @unchecked Sendable { - static let shared = TemporalAA() - private init() {} - - private(set) var isSupported: Bool = false - private var frameIndex: Int = 0 - - /// Set to true on first use and after a history-invalidating event. - /// The resolve shader outputs current-only when this is true, preventing - /// garbage history from leaking into the first accumulated frame. - private(set) var needsReset: Bool = true - - // MARK: - Lifecycle - - /// Called by initTextureResources once TAA textures have been allocated. - /// Resets frameIndex so the Halton sequence always starts from sample 0, - /// making multi-frame renders deterministic across test runs. - func markReady() { - frameIndex = 0 - needsReset = true - isSupported = true - } - - // MARK: - Per-frame helpers - - func currentJitter() -> simd_float2 { - haltonJitterTable[frameIndex % haltonTableSize] - } - - func advanceFrame() { - frameIndex = (frameIndex + 1) % haltonTableSize - needsReset = false - } - - // MARK: - History invalidation - - /// Hard reset — flush history (teleport, scene cut, viewport resize). - func reset() { - needsReset = true - } - - private var lastCameraPosition: simd_float3 = .zero - - /// Soft reset — auto-invalidate when the camera jumps more than `threshold` metres. - func checkAndResetIfNeeded(cameraPosition: simd_float3, threshold: Float = 2.0) { - if simd_length(cameraPosition - lastCameraPosition) > threshold { reset() } - lastCameraPosition = cameraPosition - } -} diff --git a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift index 79edd83e..9ce0b23b 100644 --- a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift +++ b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift @@ -216,8 +216,7 @@ class BaseRenderSetup: XCTestCase { targetName == "Bloom" || targetName == "Vignette" || targetName == "ColorGrading" || - targetName == "FXAA" || - targetName == "TAA" + targetName == "FXAA" { mode = "rgb" } else { @@ -419,8 +418,7 @@ class BaseRenderSetup: XCTestCase { referenceName == "Bloom" || referenceName == "Vignette" || referenceName == "ColorGrading" || - referenceName == "FXAA" || - referenceName == "TAA" + referenceName == "FXAA" { chosenMode = "rgb" } else { diff --git a/Tests/UntoldEngineRenderTests/PostFXTests.swift b/Tests/UntoldEngineRenderTests/PostFXTests.swift index 82c9357f..efa0fb11 100644 --- a/Tests/UntoldEngineRenderTests/PostFXTests.swift +++ b/Tests/UntoldEngineRenderTests/PostFXTests.swift @@ -36,7 +36,6 @@ final class PostFXTests: BaseRenderSetup { PostFX.enableChromaticAberration(false) PostFX.enableDepthOfField(false) FXAAParams.shared.enabled = false - TAAParams.shared.enabled = false } // MARK: - Parameter helpers diff --git a/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift b/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift index b5ed90c8..adbe62c9 100644 --- a/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift +++ b/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift @@ -15,14 +15,9 @@ import XCTest final class RenderGraphBuilderTest: BaseRenderSetup { override func setUp() async throws { try await super.setUp() - // Explicit state — don't rely on global defaults so tests are self-contained. - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = false } override func tearDown() async throws { - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = false try await super.tearDown() } @@ -71,7 +66,6 @@ final class RenderGraphBuilderTest: BaseRenderSetup { // Verify all passes are created XCTAssertNotNil(graph["model"], "Model pass should be created") XCTAssertNotNil(graph["batchedModel"], "Batched model pass should be created") - XCTAssertNotNil(graph["hzbDepthSource"], "HZB depth source pass should be created") XCTAssertNotNil(graph["ssao"], "SSAO pass should be created (handles blur internally)") XCTAssertNotNil(graph["lightPass"], "Light pass should be created") } @@ -86,12 +80,11 @@ final class RenderGraphBuilderTest: BaseRenderSetup { // Verify dependencies XCTAssertEqual(graph["model"]?.dependencies, ["shadow"], "Model pass should depend on shadow pass") XCTAssertEqual(graph["batchedModel"]?.dependencies, ["model"], "Batched model pass should depend on model pass") - XCTAssertEqual(graph["hzbDepthSource"]?.dependencies, ["batchedModel"], "HZB depth source pass should depend on batched model pass") XCTAssertEqual(graph["ssao"]?.dependencies, ["batchedModel"], "SSAO pass should depend on batched model pass") let lightDeps = graph["lightPass"]?.dependencies.sorted() - let expectedLightDeps = ["hzbDepthSource", "model", "shadow", "ssao"].sorted() - XCTAssertEqual(lightDeps, expectedLightDeps, "Light pass should depend on hzbDepthSource, model, shadow, and ssao") + let expectedLightDeps = ["batchedModel", "model", "shadow", "ssao"].sorted() + XCTAssertEqual(lightDeps, expectedLightDeps, "Light pass should depend on batchedModel, model, shadow, and ssao") } func testGBufferPass_TopologicalOrder() throws { @@ -108,11 +101,10 @@ final class RenderGraphBuilderTest: BaseRenderSetup { assertTopologicalConstraints(order: order, constraints: [ ("shadow", "model"), ("model", "batchedModel"), - ("batchedModel", "hzbDepthSource"), ("batchedModel", "ssao"), ("shadow", "lightPass"), ("model", "lightPass"), - ("hzbDepthSource", "lightPass"), + ("batchedModel", "lightPass"), ("ssao", "lightPass"), ]) } @@ -196,7 +188,6 @@ final class RenderGraphBuilderTest: BaseRenderSetup { XCTAssertNotNil(graph["environment"], "Environment pass should exist") XCTAssertNotNil(graph["shadow"], "Shadow pass should exist") XCTAssertNotNil(graph["model"], "Model pass should exist") - XCTAssertNotNil(graph["hzbDepthSource"], "HZB depth source pass should exist") XCTAssertNotNil(graph["lightPass"], "Light pass should exist") XCTAssertNotNil(graph["transparency"], "Transparency pass should exist") XCTAssertNotNil(graph["spatialDebug"], "Spatial debug pass should exist") @@ -205,13 +196,6 @@ final class RenderGraphBuilderTest: BaseRenderSetup { XCTAssertNotNil(graph["look"], "Look pass should exist") XCTAssertNotNil(graph["outputTransform"], "Output transform pass should exist") - // TAA is enabled by default — verify TAA and velocity passes are present, - // and FXAA (mutually exclusive with TAA) is absent. - XCTAssertNotNil(graph["taa"], "TAA pass should exist when TAA is enabled") - XCTAssertNotNil(graph["velocity"], "Velocity pass should exist when TAA is enabled") - XCTAssertNotNil(graph["taaPostProcessSource"], "TAA source handoff pass should exist when TAA is enabled") - XCTAssertNil(graph["fxaa"], "FXAA pass must not exist when TAA takes priority") - // Verify final pass XCTAssertEqual(finalPassID, "outputTransform", "Final pass should be outputTransform") } @@ -275,11 +259,7 @@ final class RenderGraphBuilderTest: BaseRenderSetup { ("model", "lightPass"), ("lightPass", "transparency"), ("transparency", "spatialDebug"), - ("spatialDebug", "taa"), - ("batchedModel", "velocity"), - ("velocity", "taa"), - ("taa", "taaPostProcessSource"), - ("taaPostProcessSource", "depthOfField"), + ("spatialDebug", "depthOfField"), ("depthOfField", "chromatic"), ("chromatic", "bloomThreshold"), ("bloomThreshold", "precomp"), @@ -289,66 +269,41 @@ final class RenderGraphBuilderTest: BaseRenderSetup { ]) } - func testBuildGameModeGraph_BypassPostProcessing_WithTAA() { + func testBuildGameModeGraph_BypassPostProcessing_UsesBypassPass() { renderInfo.immersionStyle = .none renderEnvironment = true bypassPostProcessing = true defer { bypassPostProcessing = false } - // TAA is enabled (setUp default) — verifies the bypass path with TAA active. let (graph, finalPassID) = buildGameModeGraph() XCTAssertEqual(finalPassID, "outputTransform", "Final pass should be outputTransform") - - // Post-processing chain replaced by bypass pass. + XCTAssertNotNil(graph["spatialDebug"], "Spatial debug pass should exist") + XCTAssertEqual(graph["spatialDebug"]?.dependencies, ["transparency"], + "Spatial debug pass should depend on transparency") XCTAssertNotNil(graph["postProcessBypass"], "Bypass pass should exist when bypassPostProcessing is enabled") - XCTAssertEqual(graph["postProcessBypass"]?.dependencies, ["taaPostProcessSource"], - "Bypass pass should depend on the TAA post-process source handoff") - XCTAssertNil(graph["depthOfField"], "DepthOfField should not exist when bypassing post-processing") - XCTAssertNil(graph["chromatic"], "Chromatic should not exist when bypassing post-processing") - XCTAssertNil(graph["bloomThreshold"], "BloomThreshold should not exist when bypassing post-processing") + XCTAssertEqual(graph["postProcessBypass"]?.dependencies, ["spatialDebug"], + "Bypass pass should depend on spatialDebug") + XCTAssertNotNil(graph["look"], "Look pass should exist when bypassing post-processing") + XCTAssertNotNil(graph["fxaa"], "FXAA pass should exist when bypassing post-processing") + XCTAssertNotNil(graph["outputTransform"], "Output transform should exist when bypassing post-processing") - // Precomp still depends on both the bypass pass and gaussian. - let precompDeps = graph["precomp"]?.dependencies.sorted() ?? [] - XCTAssertTrue(precompDeps.contains("postProcessBypass"), "Precomp should depend on postProcessBypass") - XCTAssertTrue(precompDeps.contains("gaussian"), "Precomp should still depend on gaussian") - - // TAA output chain: spatialDebug + velocity -> taa -> postProcessBypass -> precomp -> look. - XCTAssertNotNil(graph["taa"], "TAA pass should exist") - XCTAssertNotNil(graph["velocity"], "Velocity pass should exist alongside TAA") - XCTAssertNotNil(graph["taaPostProcessSource"], "TAA source handoff pass should exist") - XCTAssertNil(graph["fxaa"], "FXAA must not exist when TAA takes priority") - XCTAssertEqual(graph["look"]?.dependencies, ["precomp"], - "Look should depend on precomp") - let taaDeps = graph["taa"]?.dependencies.sorted() ?? [] - XCTAssertTrue(taaDeps.contains("spatialDebug"), "TAA should depend on spatialDebug") - XCTAssertTrue(taaDeps.contains("velocity"), "TAA should depend on velocity") - XCTAssertEqual(graph["outputTransform"]?.dependencies, ["look"], - "Output transform should depend on look after TAA feeds post-processing") - } - - func testBuildGameModeGraph_BypassPostProcessing_WithFXAA() { - renderInfo.immersionStyle = .none - renderEnvironment = true - bypassPostProcessing = true - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = true - defer { - bypassPostProcessing = false - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = false - } + XCTAssertNil(graph["depthOfField"], "Depth of field pass should not exist when bypassing post-processing") + XCTAssertNil(graph["chromatic"], "Chromatic pass should not exist when bypassing post-processing") + XCTAssertNil(graph["bloomThreshold"], "Bloom threshold pass should not exist when bypassing post-processing") - let (graph, finalPassID) = buildGameModeGraph() + let precompDeps = graph["precomp"]?.dependencies.sorted() ?? [] + XCTAssertTrue(precompDeps.contains("postProcessBypass"), + "Precomp should depend on postProcessBypass when bypassing post-processing") + XCTAssertTrue(precompDeps.contains("gaussian"), + "Precomp should still depend on gaussian pass") - XCTAssertEqual(finalPassID, "outputTransform", "Final pass should be outputTransform") - XCTAssertNotNil(graph["fxaa"], "FXAA pass should exist when TAA is disabled and FXAA is enabled") - XCTAssertNil(graph["taa"], "TAA pass must not exist when TAA is disabled") - XCTAssertNil(graph["velocity"], "Velocity pass must not exist when TAA is disabled") + XCTAssertEqual(graph["look"]?.dependencies, ["precomp"], + "Look should depend on precomp when bypassing post-processing") XCTAssertEqual(graph["fxaa"]?.dependencies, ["look"], - "FXAA should depend on look") + "FXAA should depend on look when bypassing post-processing") XCTAssertEqual(graph["outputTransform"]?.dependencies, ["fxaa"], - "Output transform should depend on fxaa") + "Output transform should depend on fxaa when bypassing post-processing") } // MARK: - Gaussian Pass Integration Tests @@ -445,115 +400,6 @@ final class RenderGraphBuilderTest: BaseRenderSetup { "Gaussian pass must come before pre-composite pass") } - // MARK: - TAA in the render graph - - func testBuildGameModeGraph_TAAEnabled_CorrectDependencies() { - renderInfo.immersionStyle = .none - renderEnvironment = true - - let (graph, _) = buildGameModeGraph() - - // velocity reads the G-buffer position written by batchedModel. - XCTAssertEqual(graph["velocity"]?.dependencies, ["batchedModel"], - "Velocity pass should depend on batchedModel") - - // taa needs the lit scene before post-processing plus motion vectors. - let taaDeps = graph["taa"]?.dependencies.sorted() ?? [] - XCTAssertTrue(taaDeps.contains("spatialDebug"), "TAA should depend on spatialDebug") - XCTAssertTrue(taaDeps.contains("velocity"), "TAA should depend on velocity") - - XCTAssertEqual(graph["taaPostProcessSource"]?.dependencies, ["taa"], - "TAA handoff should depend on the resolved TAA output") - XCTAssertEqual(graph["postProcessDisabledBypass"]?.dependencies, ["taaPostProcessSource"], - "Post-processing should read from the TAA handoff when effects are disabled") - XCTAssertEqual(graph["outputTransform"]?.dependencies, ["look"], - "outputTransform should depend on look after TAA feeds post-processing") - } - - func testBuildGameModeGraph_TAAEnabled_TopologicalOrder() throws { - renderInfo.immersionStyle = .none - renderEnvironment = true - - let (graph, _) = buildGameModeGraph() - let sorted = try topologicalSortGraph(graph: graph) - let order = sorted.map(\.id) - - assertTopologicalConstraints(order: order, constraints: [ - ("batchedModel", "velocity"), - ("spatialDebug", "taa"), - ("velocity", "taa"), - ("taa", "taaPostProcessSource"), - ("taaPostProcessSource", "postProcessDisabledBypass"), - ("postProcessDisabledBypass", "precomp"), - ("precomp", "look"), - ("look", "outputTransform"), - ]) - } - - func testBuildGameModeGraph_TAADisabled_NoTAAOrVelocityPass() { - renderInfo.immersionStyle = .none - renderEnvironment = true - TAAParams.shared.enabled = false - defer { TAAParams.shared.enabled = true } - - let (graph, _) = buildGameModeGraph() - - XCTAssertNil(graph["taa"], "TAA pass must not exist when TAA is disabled") - XCTAssertNil(graph["velocity"], "Velocity pass must not exist when TAA is disabled") - } - - func testBuildGameModeGraph_TAADisabled_OutputDependsOnLook() { - renderInfo.immersionStyle = .none - renderEnvironment = true - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = false - defer { - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = false - } - - let (graph, _) = buildGameModeGraph() - - XCTAssertEqual(graph["outputTransform"]?.dependencies, ["look"], - "outputTransform should depend directly on look when both TAA and FXAA are off") - } - - func testBuildGameModeGraph_TAAAndFXAABothEnabled_TAAWins() { - renderInfo.immersionStyle = .none - renderEnvironment = true - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = true - defer { FXAAParams.shared.enabled = false } - - let (graph, _) = buildGameModeGraph() - - XCTAssertNotNil(graph["taa"], "TAA pass should exist when both AA flags are enabled") - XCTAssertNil(graph["fxaa"], "FXAA pass must not exist when TAA takes priority") - XCTAssertEqual(graph["outputTransform"]?.dependencies, ["look"], - "outputTransform should depend on look because TAA feeds post-processing") - } - - func testBuildGameModeGraph_TAADisabled_FXAAEnabled_CreatesFXAAPass() { - renderInfo.immersionStyle = .none - renderEnvironment = true - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = true - defer { - TAAParams.shared.enabled = true - FXAAParams.shared.enabled = false - } - - let (graph, _) = buildGameModeGraph() - - XCTAssertNotNil(graph["fxaa"], "FXAA pass should exist when TAA is off and FXAA is on") - XCTAssertNil(graph["taa"], "TAA pass must not exist when TAA is disabled") - XCTAssertNil(graph["velocity"], "Velocity pass must not exist when TAA is disabled") - XCTAssertEqual(graph["fxaa"]?.dependencies, ["look"], - "FXAA should depend on look") - XCTAssertEqual(graph["outputTransform"]?.dependencies, ["fxaa"], - "outputTransform should depend on fxaa") - } - // MARK: - Helper Methods func assertTopologicalConstraints( diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png index db97fb5f..f7d7115d 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png index f64079d7..2b585483 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png index da5d0355..83155b03 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png index d4106891..64b25d70 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png index 4be96c3b..bed6b4fd 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png index 90e580d8..a618e776 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png index c41e5914..3e3f089b 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/SSAOReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TAAReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TAAReference.png deleted file mode 100644 index 9259c96a..00000000 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TAAReference.png and /dev/null differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png index d4106891..64b25d70 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/TemporalAATests.swift b/Tests/UntoldEngineRenderTests/TemporalAATests.swift deleted file mode 100644 index afc6c195..00000000 --- a/Tests/UntoldEngineRenderTests/TemporalAATests.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// TemporalAATests.swift -// UntoldEngine -// -// Copyright (C) Untold Engine Studios -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import simd -@testable import UntoldEngine -import XCTest - -final class TemporalAATests: BaseRenderSetup { - override func setUp() async throws { - try await super.setUp() - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = false - } - - override func tearDown() async throws { - TAAParams.shared.enabled = false - FXAAParams.shared.enabled = false - try await super.tearDown() - } - - // MARK: - Halton jitter table - - func testHaltonTableHasCorrectSize() { - XCTAssertEqual(haltonJitterTable.count, haltonTableSize, - "Halton table must have exactly haltonTableSize entries") - } - - func testHaltonTableValuesAreInRange() { - for (i, sample) in haltonJitterTable.enumerated() { - XCTAssertGreaterThanOrEqual(sample.x, -0.5, "Sample \(i) x is below -0.5") - XCTAssertLessThanOrEqual(sample.x, 0.5, "Sample \(i) x is above 0.5") - XCTAssertGreaterThanOrEqual(sample.y, -0.5, "Sample \(i) y is below -0.5") - XCTAssertLessThanOrEqual(sample.y, 0.5, "Sample \(i) y is above 0.5") - } - } - - func testHaltonTableHasNoRepeats() { - let keys = haltonJitterTable.map { "\($0.x),\($0.y)" } - XCTAssertEqual(Set(keys).count, haltonJitterTable.count, - "Halton table should not contain duplicate samples") - } - - func testCurrentJitterCyclesAfterFullTable() { - let first = TemporalAA.shared.currentJitter() - for _ in 0 ..< haltonTableSize { - TemporalAA.shared.advanceFrame() - } - let afterCycle = TemporalAA.shared.currentJitter() - XCTAssertEqual(first.x, afterCycle.x, accuracy: 1e-6, - "Jitter X should return to the same value after one full cycle") - XCTAssertEqual(first.y, afterCycle.y, accuracy: 1e-6, - "Jitter Y should return to the same value after one full cycle") - } - - // MARK: - TemporalAA state machine - - func testMarkReadySetsIsSupported() { - // initSizeableResources (called from setUp) already invokes markReady. - XCTAssertTrue(TemporalAA.shared.isSupported, - "isSupported must be true after initSizeableResources") - } - - func testMarkReadySetsNeedsReset() { - TemporalAA.shared.markReady() - XCTAssertTrue(TemporalAA.shared.needsReset, - "markReady should arm needsReset so the first frame skips history") - } - - func testAdvanceFrameClearsNeedsReset() { - TemporalAA.shared.markReady() - XCTAssertTrue(TemporalAA.shared.needsReset) - TemporalAA.shared.advanceFrame() - XCTAssertFalse(TemporalAA.shared.needsReset, - "advanceFrame should clear needsReset after the first frame") - } - - func testResetSetsNeedsReset() { - TemporalAA.shared.markReady() - TemporalAA.shared.advanceFrame() - XCTAssertFalse(TemporalAA.shared.needsReset) - TemporalAA.shared.reset() - XCTAssertTrue(TemporalAA.shared.needsReset, - "reset() must re-arm needsReset to flush stale history") - } - - // MARK: - Camera-jump auto-reset - - func testCheckAndResetTriggersOnLargeJump() { - // Establish a known baseline position then clear the reset flag. - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: .zero) - TemporalAA.shared.advanceFrame() - XCTAssertFalse(TemporalAA.shared.needsReset) - - // Jump 3 m — exceeds the 2 m default threshold. - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: simd_float3(0, 0, 3)) - XCTAssertTrue(TemporalAA.shared.needsReset, - "A camera jump > 2 m should trigger a history reset") - } - - func testCheckAndResetIgnoresSmallMovement() { - // Establish a known baseline position then clear the reset flag. - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: .zero) - TemporalAA.shared.advanceFrame() - XCTAssertFalse(TemporalAA.shared.needsReset) - - // Move 0.5 m — below the 2 m threshold. - TemporalAA.shared.checkAndResetIfNeeded(cameraPosition: simd_float3(0, 0, 0.5)) - XCTAssertFalse(TemporalAA.shared.needsReset, - "A camera move < 2 m must not trigger a history reset") - } - - // MARK: - GPU: texture allocation - - func testTAATexturesExistAfterInit() { - XCTAssertNotNil(textureResources.taaOutputTexture, - "taaOutputTexture must be allocated after initSizeableResources") - XCTAssertNotNil(textureResources.taaHistoryTexture, - "taaHistoryTexture must be allocated after initSizeableResources") - XCTAssertNotNil(textureResources.taaPositionHistoryTexture, - "taaPositionHistoryTexture must be allocated after initSizeableResources") - XCTAssertNotNil(textureResources.velocityTexture, - "velocityTexture must be allocated after initSizeableResources") - } - - func testTAATexturesMatchViewportDimensions() { - guard let out = textureResources.taaOutputTexture, - let hist = textureResources.taaHistoryTexture, - let pos = textureResources.taaPositionHistoryTexture - else { - XCTFail("TAA textures must exist before checking dimensions") - return - } - for (label, tex) in [("output", out), ("history", hist), ("positionHistory", pos)] { - XCTAssertEqual(tex.width, windowWidth, "\(label) width must match viewport") - XCTAssertEqual(tex.height, windowHeight, "\(label) height must match viewport") - } - } - - // MARK: - Reference image generation (uncomment to regenerate) - - func generateTAAReferenceImage() { - XCTAssertNotNil(renderer) - TAAParams.shared.enabled = true - for _ in 0 ..< haltonTableSize { - renderer.draw(in: renderer.metalView) - } - let exp = expectation(description: "TAA ref") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - if let tex = textureResources.taaOutputTexture { - self.testGenerateRenderTarget(targetName: "TAA", texture: tex) - } - exp.fulfill() - } - wait(for: [exp], timeout: TimeInterval(timeoutFactor)) - } - - // MARK: - GPU: PSNR convergence - - func testTAAOutput() { - XCTAssertNotNil(renderer, "Renderer must be initialized") - TAAParams.shared.enabled = true - - // Render one full Halton cycle (16 frames) so the history is converged - // before we sample the output for comparison. A single-frame test is - // not meaningful for TAA because frame 0 always outputs current-only - // (needsReset = true skips history blending). - for _ in 0 ..< haltonTableSize { - renderer.draw(in: renderer.metalView) - } - - let exp = expectation(description: "TAA PSNR") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - guard let tex = textureResources.taaOutputTexture else { - XCTFail("taaOutputTexture must exist after enabling TAA") - exp.fulfill() - return - } - self.psnrTest(targetName: "TAA", texture: tex) - exp.fulfill() - } - wait(for: [exp], timeout: TimeInterval(timeoutFactor)) - } -} diff --git a/Tests/UntoldEngineRenderTests/TransparencyTests.swift b/Tests/UntoldEngineRenderTests/TransparencyTests.swift index edbc6c6f..a2b4e43d 100644 --- a/Tests/UntoldEngineRenderTests/TransparencyTests.swift +++ b/Tests/UntoldEngineRenderTests/TransparencyTests.swift @@ -291,12 +291,7 @@ final class TransparencyRenderGraphTests: BaseRenderSetup { renderInfo.immersionStyle = .none renderEnvironment = true bypassPostProcessing = true - let savedTAA = TAAParams.shared.enabled - TAAParams.shared.enabled = false - defer { - bypassPostProcessing = false - TAAParams.shared.enabled = savedTAA - } + defer { bypassPostProcessing = false } let (graph, _) = buildGameModeGraph() diff --git a/scripts/next-version.sh b/scripts/next-version.sh index 4900fea5..b8a3f446 100755 --- a/scripts/next-version.sh +++ b/scripts/next-version.sh @@ -128,6 +128,12 @@ if [[ "${DO_CLIFF}" == "true" ]]; then sed -i '' 's/Logger\.log(message: "Untold Engine Starting[^"]*")/Logger.log(message: "Untold Engine Starting. Version '"${NEXT}"'")/' \ Sources/UntoldEngine/Renderer/UntoldEngine.swift echo "Updated startup log to ${NEXT} in UntoldEngine." + + # Update stable release tag in README and GettingStarted doc + sed -i '' 's/git checkout v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/git checkout v'"${NEXT}"'/' \ + README.md \ + docs/API/GettingStarted.md + echo "Updated checkout tag to v${NEXT} in README.md and docs/API/GettingStarted.md." fi # Optionally run Docusaurus docs:version