diff --git a/Sources/DemoGame/main.swift b/Sources/DemoGame/main.swift index d639082a..4ceef8e2 100644 --- a/Sources/DemoGame/main.swift +++ b/Sources/DemoGame/main.swift @@ -161,8 +161,11 @@ // Input (WASD) for the demo InputSystem.shared.registerKeyboardEvents() + // bypass Post FX effects + bypassPostProcessing = true + // Disable SSAO - SSAOParams.shared.enabled = true + SSAOParams.shared.enabled = false // Test Fast quality (8 samples, half-res) SSAOParams.shared.quality = .high } @@ -219,7 +222,7 @@ // Step 1. Create and configure the window window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + contentRect: NSRect(x: 0, y: 0, width: 1920, height: 1080), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false diff --git a/Sources/UntoldEngine/Renderer/RenderPasses.swift b/Sources/UntoldEngine/Renderer/RenderPasses.swift index 77ea1124..fb4c9d20 100644 --- a/Sources/UntoldEngine/Renderer/RenderPasses.swift +++ b/Sources/UntoldEngine/Renderer/RenderPasses.swift @@ -1266,99 +1266,6 @@ public enum RenderPasses { textureResources.shadowMap, index: Int(lightPassShadowTextureIndex.rawValue) ) - // point lights - /* - if let pointLightBuffer = bufferResources.pointLightBuffer { - - let headerSize = MemoryLayout.stride * 4 - let MAX_POINT_LIGHTS = 1024 - - // Grab lights and cap to buffer capacity - let src = getPointLights() - let capped = min(src.count, MAX_POINT_LIGHTS) - - // Write count at offset 0 - pointLightBuffer.contents().storeBytes(of: UInt32(capped), toByteOffset: 0, as: UInt32.self) - - // Copy the contiguous bytes of the array after the header - let dst = pointLightBuffer.contents().advanced(by: headerSize) - - src.withUnsafeBytes{ raw in - let stride = MemoryLayout.stride - let nBytes = capped * stride - dst.copyMemory(from: raw.baseAddress!, byteCount: nBytes) - } - - renderEncoder.setFragmentBuffer( - pointLightBuffer, offset: 0, index: Int(lightPassPointLightsIndex.rawValue) - ) - } else { - handleError(.bufferAllocationFailed, bufferResources.pointLightBuffer!.label!) - return - } - - // spot light - if let spotLightBuffer = bufferResources.spotLightBuffer { - - let headerSize = MemoryLayout.stride * 4 - let MAX_POINT_LIGHTS = 1024 - - // Grab lights and cap to buffer capacity - let src = getSpotLights() - let capped = min(src.count, MAX_POINT_LIGHTS) - - // Write count at offset 0 - spotLightBuffer.contents().storeBytes(of: UInt32(capped), toByteOffset: 0, as: UInt32.self) - - // Copy the contiguous bytes of the array after the header - let dst = spotLightBuffer.contents().advanced(by: headerSize) - - src.withUnsafeBytes{ raw in - let stride = MemoryLayout.stride - let nBytes = capped * stride - dst.copyMemory(from: raw.baseAddress!, byteCount: nBytes) - } - - renderEncoder.setFragmentBuffer( - spotLightBuffer, offset: 0, index: Int(lightPassSpotLightsIndex.rawValue) - ) - - } else { - handleError(.bufferAllocationFailed, bufferResources.spotLightBuffer!.label!) - return - } - - // area light - if let areaLightBuffer = bufferResources.areaLightBuffer { - - let headerSize = MemoryLayout.stride * 4 - let MAX_POINT_LIGHTS = 1024 - - // Grab lights and cap to buffer capacity - let src = getAreaLights() - let capped = min(src.count, MAX_POINT_LIGHTS) - - // Write count at offset 0 - areaLightBuffer.contents().storeBytes(of: UInt32(capped), toByteOffset: 0, as: UInt32.self) - - // Copy the contiguous bytes of the array after the header - let dst = areaLightBuffer.contents().advanced(by: headerSize) - - src.withUnsafeBytes{ raw in - let stride = MemoryLayout.stride - let nBytes = capped * stride - dst.copyMemory(from: raw.baseAddress!, byteCount: nBytes) - } - - renderEncoder.setFragmentBuffer( - areaLightBuffer, offset: 0, index: Int(lightPassAreaLightsIndex.rawValue) - ) - - } else { - handleError(.bufferAllocationFailed, bufferResources.areaLightBuffer!.label!) - return - } - */ let MAX_POINT_LIGHTS = 1024 let headerSize = 16 // Point @@ -1528,8 +1435,13 @@ public enum RenderPasses { renderInfo.offscreenRenderPassDescriptor?.depthAttachment.texture, index: Int(prePassDepthTextureIndex.rawValue) ) } else { + let postProcessColorTexture = renderInfo.postProcessRenderPassDescriptor?.colorAttachments[0].texture + let finalColorTexture = bypassPostProcessing + ? (renderInfo.deferredRenderPassDescriptor?.colorAttachments[0].texture ?? postProcessColorTexture) + : postProcessColorTexture + renderEncoder.setFragmentTexture( - renderInfo.postProcessRenderPassDescriptor?.colorAttachments[0].texture, + finalColorTexture, index: Int(prePassFinalTextureIndex.rawValue) ) diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 3c3e9382..0c41830f 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -45,7 +45,7 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { let renderer = UntoldRenderer(configuration: configuration) guard let device = MTLCreateSystemDefaultDevice() else { - assertionFailure("Metal device is not available.") + Logger.logError(message: "Metal device is not available.") return nil } renderer.metalView.device = device diff --git a/Sources/UntoldEngine/Systems/RenderingSystem.swift b/Sources/UntoldEngine/Systems/RenderingSystem.swift index 1dcc008a..759037a6 100644 --- a/Sources/UntoldEngine/Systems/RenderingSystem.swift +++ b/Sources/UntoldEngine/Systems/RenderingSystem.swift @@ -165,10 +165,32 @@ public func buildGameModeGraph() -> RenderGraphResult { let gaussianPass = RenderPass(id: "gaussian", dependencies: ["model"], execute: RenderPasses.gaussianExecution) graph[gaussianPass.id] = gaussianPass - let postProcess = postProcessingEffects(graph: &graph, deferredPassId: "lightPass", geometryPassId: "model") + let postProcessID: String + if bypassPostProcessing { + let bypassPass = RenderPass( + id: "postProcessBypass", + dependencies: ["lightPass"], + execute: { _ in + guard let deferredDescriptor = renderInfo.deferredRenderPassDescriptor else { + return + } + renderInfo.postProcessRenderPassDescriptor?.colorAttachments[0].texture = + deferredDescriptor.colorAttachments[0].texture + } + ) + graph[bypassPass.id] = bypassPass + postProcessID = bypassPass.id + } else { + let postProcess = postProcessingEffects(graph: &graph, deferredPassId: "lightPass", geometryPassId: "model") + postProcessID = postProcess.id + } // PreComposite depends on both post-processing and gaussian - let preCompPass = RenderPass(id: "precomp", dependencies: [postProcess.id, gaussianPass.id], execute: RenderPasses.preCompositeExecution) + let preCompPass = RenderPass( + id: "precomp", + dependencies: [postProcessID, gaussianPass.id], + execute: RenderPasses.preCompositeExecution + ) graph[preCompPass.id] = preCompPass return (graph, preCompPass.id) diff --git a/Sources/UntoldEngine/Utils/Globals.swift b/Sources/UntoldEngine/Utils/Globals.swift index 180a829b..76005849 100644 --- a/Sources/UntoldEngine/Utils/Globals.swift +++ b/Sources/UntoldEngine/Utils/Globals.swift @@ -206,6 +206,7 @@ public let commandBufferSemaphore = DispatchSemaphore(value: maxInFlightCommandB // Engine profiling/benchmarking public var enableEngineMetrics: Bool = false +public var bypassPostProcessing = false public class ToneMappingParams: ObservableObject { static let shared = ToneMappingParams() diff --git a/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift b/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift index 085725f9..31cee03c 100644 --- a/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift +++ b/Tests/UntoldEngineRenderTests/RenderGraphBuilderTest.swift @@ -254,6 +254,30 @@ final class RenderGraphBuilderTest: BaseRenderSetup { ]) } + func testBuildGameModeGraph_BypassPostProcessing_UsesBypassPass() { + renderInfo.immersionStyle = .none + renderEnvironment = true + bypassPostProcessing = true + defer { bypassPostProcessing = false } + + let (graph, finalPassID) = buildGameModeGraph() + + XCTAssertEqual(finalPassID, "precomp", "Final pass should be precomp") + XCTAssertNotNil(graph["postProcessBypass"], "Bypass pass should exist when bypassPostProcessing is enabled") + XCTAssertEqual(graph["postProcessBypass"]?.dependencies, ["lightPass"], + "Bypass pass should depend on lightPass") + + 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 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") + } + // MARK: - Gaussian Pass Integration Tests func testBuildGameModeGraph_GaussianPassExists() {