From 9126efe4dfdfce03ccb5156df6e51c9ba2b3b1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?furby=E2=84=A2?= Date: Sun, 29 Dec 2024 08:34:36 -0700 Subject: [PATCH] Add metal blitting with hgi texture. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: furby™ --- Sources/PixarUSD/Usd/Usd/Attribute.swift | 2 + .../include/UsdImagingGL/engine.h | 2 +- .../UsdView/Hydra/Hydra+RenderEngine.swift | 39 ++++- .../UsdView/Hydra/MTL/Hydra+MTLRenderer.swift | 143 ++++++++++++++---- .../Resources/Shaders/BlitShaders.metal | 56 +++++++ 5 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 Sources/UsdView/Resources/Shaders/BlitShaders.metal diff --git a/Sources/PixarUSD/Usd/Usd/Attribute.swift b/Sources/PixarUSD/Usd/Usd/Attribute.swift index 6f5b2591e..224f558ea 100644 --- a/Sources/PixarUSD/Usd/Usd/Attribute.swift +++ b/Sources/PixarUSD/Usd/Usd/Attribute.swift @@ -27,11 +27,13 @@ public extension Usd.Attribute SetDocumentation(std.string(doc)) } + @discardableResult func set(_ value: String, time: UsdTimeCode = UsdTimeCode.Default()) -> Bool { Set(std.string(value), time) } + @discardableResult func set(_ value: Sdf.AssetPath, time: UsdTimeCode = UsdTimeCode.Default()) -> Bool { Set(value, time) diff --git a/Sources/UsdImagingGL/include/UsdImagingGL/engine.h b/Sources/UsdImagingGL/include/UsdImagingGL/engine.h index 5b335247e..fd6ea6246 100644 --- a/Sources/UsdImagingGL/include/UsdImagingGL/engine.h +++ b/Sources/UsdImagingGL/include/UsdImagingGL/engine.h @@ -452,7 +452,7 @@ class UsdImagingGLEngine { /// Returns an AOV texture handle for the given token. USDIMAGINGGL_API - HgiTextureHandle GetAovTexture(TfToken const &name) const; + HgiTextureHandle GetAovTexture(TfToken const &name) const SWIFT_RETURNS_INDEPENDENT_VALUE; /// Returns the AOV render buffer for the given token. USDIMAGINGGL_API diff --git a/Sources/UsdView/Hydra/Hydra+RenderEngine.swift b/Sources/UsdView/Hydra/Hydra+RenderEngine.swift index 78be4f4e1..29653a3b4 100644 --- a/Sources/UsdView/Hydra/Hydra+RenderEngine.swift +++ b/Sources/UsdView/Hydra/Hydra+RenderEngine.swift @@ -43,23 +43,54 @@ public enum Hydra ) } - public func render(rgba: (Double, Double, Double, Double)) + public func render(at timeCode: Double, viewSize: CGSize) -> Pixar.HgiTextureHandle { - var params = UsdImagingGL.RenderParams() + // Draws the scene using Hydra. + // Camera projection setup. + // let cameraTransform = viewCamera.getTransform() + // let cameraParams = viewCamera.getShaderParams() + // let frustum = computeFrustum(cameraTransform: cameraTransform, viewSize: viewSize, params: cameraParams) + // let modelViewMatrix = frustum.computeViewMatrix() + // let projMatrix = frustum.computeProjectionMatrix() + // engine.setCameraState(modelViewMatrix: modelViewMatrix, projMatrix: projMatrix) + + // Viewport setup. + let viewport = Gf.Vec4d(0, 0, viewSize.width, viewSize.height) + engine.pointee.SetRenderViewport(viewport) + engine.pointee.SetWindowPolicy(Pixar.CameraUtilConformWindowPolicy.init(0)) - params.frame = Usd.TimeCode.Default() - params.clearColor = .init(Float(rgba.0), Float(rgba.1), Float(rgba.2), Float(rgba.3)) + var params = UsdImagingGL.RenderParams() + params.frame = Usd.TimeCode(timeCode) + params.clearColor = .init(Float(0.2), Float(0.2), Float(0.2), Float(1.0)) params.enableIdRender = false params.showGuides = true params.showRender = true params.showProxy = true + // Light and material setup. + // let lights = computeLights(cameraTransform: cameraTransform) + // engine.setLightingState(lights: lights, material: material, sceneAmbient: sceneAmbient) + + // Render the frame. engine.render(root: stage.getPseudoRoot(), params: params) + + // Return the color output. + return engine.pointee.GetAovTexture(Hd.AovTokens.color.token) } public var hydraDevice: MTLDevice { hgi.device } + + public func getHgi() -> Pixar.HgiMetalPtr + { + hgi + } + + public func getEngine() -> UsdImagingGL.EngineSharedPtr + { + engine + } } } diff --git a/Sources/UsdView/Hydra/MTL/Hydra+MTLRenderer.swift b/Sources/UsdView/Hydra/MTL/Hydra+MTLRenderer.swift index bfefa717c..4a970b665 100644 --- a/Sources/UsdView/Hydra/MTL/Hydra+MTLRenderer.swift +++ b/Sources/UsdView/Hydra/MTL/Hydra+MTLRenderer.swift @@ -22,9 +22,11 @@ import PixarUSD { private let device: MTLDevice private var hydra: Hydra.RenderEngine? - //private let commandQueue: MTLCommandQueue + /// private let commandQueue: MTLCommandQueue private var pipelineState: MTLRenderPipelineState? + private var inFlightSemaphore = DispatchSemaphore(value: 1) + convenience init(device: MTLDevice, hydra: Hydra.RenderEngine) { self.init(device: device)! @@ -41,7 +43,7 @@ import PixarUSD // self.commandQueue = commandQueue super.init() - setupPipeline() + loadMetal() } private func setupPipeline() @@ -63,33 +65,120 @@ import PixarUSD public func draw(in view: MTKView) { - guard let drawable = view.currentDrawable else { return } - guard let renderPassDescriptor = view.currentRenderPassDescriptor else { return } - - let commandQueue = view.device?.makeCommandQueue() - let commandBuffer = commandQueue?.makeCommandBuffer() - let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) - - if let pipelineState { - renderEncoder?.setRenderPipelineState(pipelineState) + drawFrame(in: view, timeCode: 0.0) + } + + /// draw the scene, and blit the result to the view. + func drawFrame(in view: MTKView, timeCode: Double) -> Bool + { + guard let hgi = hydra?.getHgi() else { return false } + + // start the next frame. + _ = inFlightSemaphore.wait(timeout: .distantFuture) + defer { inFlightSemaphore.signal() } + + hgi.pointee.StartFrame() + + // draw the scene using Hydra, and recast the result to a MTLTexture. + let viewSize = view.drawableSize + guard + let hgiTexture = hydra?.render(at: timeCode, viewSize: viewSize), + let metalTexture = hgiTexture.GetId() as? MTLTexture, + let commandBuffer = hgi.pointee.GetPrimaryCommandBuffer() + else { return false } + + // copy the rendered texture to the view. + blitToView(view, commandBuffer: commandBuffer, texture: metalTexture) + + // tell Hydra to commit the command buffer and complete the work. + hgi.pointee.CommitPrimaryCommandBuffer() + hgi.pointee.EndFrame() + + return true + } + + /// copies the texture to the view with a shader. + public func blitToView(_ view: MTKView, commandBuffer: MTLCommandBuffer, texture: MTLTexture) + { + guard let renderPassDescriptor = view.currentRenderPassDescriptor + else + { + return + } + + // Create a render command encoder to encode the copy command. + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + else + { + return } - renderEncoder?.setViewport( - MTLViewport( - originX: 0.0, - originY: 0.0, - width: Double(view.drawableSize.width), - height: Double(view.drawableSize.height), - znear: 0.0, - zfar: 1.0 - ) - ) - - hydra?.render(rgba: (0.1, 0.1, 0.1, 1.0)) - - renderEncoder?.endEncoding() - commandBuffer?.present(drawable) - commandBuffer?.commit() + // Blit the texture to the view. + renderEncoder.pushDebugGroup("FinalBlit") + renderEncoder.setFragmentTexture(texture, index: 0) + if let pipelineState + { + renderEncoder.setRenderPipelineState(pipelineState) + } + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) + renderEncoder.popDebugGroup() + + // Finish encoding the copy command. + renderEncoder.endEncoding() + if let drawable = view.currentDrawable + { + commandBuffer.present(drawable) + } + } + + /// Prepares the Metal objects for copying to the view. + private func loadMetal() + { + do + { + let defaultLibrary = try device.makeDefaultLibrary(bundle: .usdview) + + guard let vertexFunction = defaultLibrary.makeFunction(name: "vtxBlit") + else + { + print("Failed to create vertex function.") + return + } + + guard let fragmentFunction = defaultLibrary.makeFunction(name: "fragBlitLinear") + else + { + print("Failed to create fragment function.") + return + } + + // Set up the pipeline state descriptor. + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.rasterSampleCount = 1 + pipelineStateDescriptor.vertexFunction = vertexFunction + pipelineStateDescriptor.fragmentFunction = fragmentFunction + pipelineStateDescriptor.depthAttachmentPixelFormat = .invalid + + // Configure the color attachment for blending. + if let colorAttachment = pipelineStateDescriptor.colorAttachments[0] + { + colorAttachment.pixelFormat = .bgra8Unorm + colorAttachment.isBlendingEnabled = true + colorAttachment.rgbBlendOperation = .add + colorAttachment.alphaBlendOperation = .add + colorAttachment.sourceRGBBlendFactor = .one + colorAttachment.sourceAlphaBlendFactor = .one + colorAttachment.destinationRGBBlendFactor = .oneMinusSourceAlpha + colorAttachment.destinationAlphaBlendFactor = .zero + } + + // Create the pipeline state object. + pipelineState = try device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) + } + catch + { + print("Failed to create pipeline state: \(error.localizedDescription)") + } } } } diff --git a/Sources/UsdView/Resources/Shaders/BlitShaders.metal b/Sources/UsdView/Resources/Shaders/BlitShaders.metal new file mode 100644 index 000000000..9ecad461d --- /dev/null +++ b/Sources/UsdView/Resources/Shaders/BlitShaders.metal @@ -0,0 +1,56 @@ +/* +Copyright © 2022 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +using namespace metal; + +struct VertexOut +{ + float4 position [[ position ]]; + float2 texcoord; +}; + +vertex VertexOut vtxBlit(uint vid [[vertex_id]]) +{ + // These vertices map a triangle to cover a fullscreen quad. + const float4 vertices[] = { + float4(-1, -1, 1, 1), // bottom left + float4(3, -1, 1, 1), // bottom right + float4(-1, 3, 1, 1), // upper left + }; + + const float2 texcoords[] = { + float2(0.0, 0.0), // bottom left + float2(2.0, 0.0), // bottom right + float2(0.0, 2.0), // upper left + }; + + VertexOut out; + out.position = vertices[vid]; + out.texcoord = texcoords[vid]; + return out; +} + +fragment half4 fragBlitLinear(VertexOut in [[stage_in]], texture2d tex[[texture(0)]]) +{ + constexpr sampler s = sampler(address::clamp_to_edge); + + float4 pixel = tex.sample(s, in.texcoord); + return half4(pixel); +}