mirror of
https://github.com/OpenEmu/OpenEmu-Shaders.git
synced 2025-11-01 11:07:54 +00:00
1266 lines
50 KiB
Swift
1266 lines
50 KiB
Swift
// Copyright (c) 2022, OpenEmu Team
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// * Neither the name of the OpenEmu Team nor the
|
|
// names of its contributors may be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
// DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import Foundation
|
|
import Metal
|
|
import MetalKit
|
|
import os.log
|
|
|
|
// swiftlint:disable type_body_length
|
|
public final class FilterChain {
|
|
enum InitError: Error {
|
|
case invalidSamplerState
|
|
}
|
|
|
|
private let device: MTLDevice
|
|
private let library: MTLLibrary
|
|
private let loader: MTKTextureLoader
|
|
|
|
private var vertex = [
|
|
Vertex(position: simd_float4(x: 0, y: 1, z: 0, w: 1), texCoord: simd_float2(x: 0, y: 1)),
|
|
Vertex(position: simd_float4(x: 1, y: 1, z: 0, w: 1), texCoord: simd_float2(x: 1, y: 1)),
|
|
Vertex(position: simd_float4(x: 0, y: 0, z: 0, w: 1), texCoord: simd_float2(x: 0, y: 0)),
|
|
Vertex(position: simd_float4(x: 1, y: 0, z: 0, w: 1), texCoord: simd_float2(x: 1, y: 0)),
|
|
]
|
|
private var vertexFlipped = [
|
|
Vertex(position: simd_float4(x: 0, y: 1, z: 0, w: 1), texCoord: simd_float2(x: 0, y: 0)),
|
|
Vertex(position: simd_float4(x: 0, y: 0, z: 0, w: 1), texCoord: simd_float2(x: 0, y: 1)),
|
|
Vertex(position: simd_float4(x: 1, y: 1, z: 0, w: 1), texCoord: simd_float2(x: 1, y: 0)),
|
|
Vertex(position: simd_float4(x: 1, y: 0, z: 0, w: 1), texCoord: simd_float2(x: 1, y: 1)),
|
|
]
|
|
let vertexSizeBytes: Int
|
|
|
|
private var samplers: SamplerFilterArray<MTLSamplerState>
|
|
|
|
public var hasShader: Bool = false
|
|
|
|
private var frameCount: UInt = 0
|
|
private var passCount: Int = 0
|
|
private var lastPassIndex: Int = 0
|
|
private var historyCount: Int = 0
|
|
|
|
// The OriginalHistory(N) semantic
|
|
private var historyTextures = [Texture](repeating: .init(), count: Constants.maxFrameHistory + 1)
|
|
|
|
private struct OutputFrame {
|
|
var viewport: MTLViewport
|
|
var outputSize: TextureSize
|
|
|
|
init() {
|
|
viewport = .init()
|
|
outputSize = .init()
|
|
}
|
|
}
|
|
|
|
private var outputFrame: OutputFrame = .init()
|
|
|
|
private struct Pass {
|
|
var format = MTLPixelFormat.bgra8Unorm
|
|
var buffers = [MTLBuffer?](repeating: nil, count: Constants.maxConstantBuffers)
|
|
var vBuffers = [MTLBuffer?](repeating: nil, count: Constants.maxConstantBuffers) // array used for vertex binding
|
|
var fBuffers = [MTLBuffer?](repeating: nil, count: Constants.maxConstantBuffers) // array used for fragment binding
|
|
var renderTarget = Texture()
|
|
var feedbackTarget = Texture()
|
|
var frameCount = UInt32(0)
|
|
var frameCountMod = UInt32(0)
|
|
var frameDirection = Int32(0)
|
|
var bindings: ShaderPassBindings?
|
|
var viewport = MTLViewport()
|
|
var state: MTLRenderPipelineState?
|
|
var hasFeedback = false
|
|
var scaleX: ShaderPassScale?
|
|
var scaleY: ShaderPassScale?
|
|
var isScaled: Bool { scaleX != nil && scaleY != nil }
|
|
|
|
func getOutputSize(viewport: CGSize, source: CGSize) -> CGSize {
|
|
let width: CGFloat
|
|
switch scaleX {
|
|
case .source(let scale):
|
|
width = source.width * scale
|
|
case .absolute(let size):
|
|
width = Double(size)
|
|
case .viewport(let scale):
|
|
width = viewport.width * scale
|
|
default:
|
|
width = source.width
|
|
}
|
|
|
|
let height: CGFloat
|
|
switch scaleY {
|
|
case .source(let scale):
|
|
height = source.height * scale
|
|
case .absolute(let size):
|
|
height = Double(size)
|
|
case .viewport(let scale):
|
|
height = viewport.height * scale
|
|
default:
|
|
height = source.height
|
|
}
|
|
|
|
return CGSize(width: width.rounded(), height: height.rounded())
|
|
}
|
|
}
|
|
|
|
private var pass = [Pass](repeating: .init(), count: Constants.maxShaderPasses)
|
|
private var luts = [Texture](repeating: .init(), count: Constants.maxTextures)
|
|
|
|
private var renderTargetsNeedResize = true
|
|
private var historyNeedsInit = false
|
|
|
|
public private(set) var sourceRect = CGRect.zero
|
|
|
|
private var aspectSize = CGSize.zero
|
|
|
|
public private(set) var outputBounds = CGRect.zero
|
|
|
|
public var frameDirection: Int = 1
|
|
|
|
// render target layer state
|
|
private let pipelineState: MTLRenderPipelineState
|
|
|
|
private var _rotation: Float = 0
|
|
|
|
private var uniforms = Uniforms.empty
|
|
private var uniformsNoRotate = Uniforms.empty
|
|
|
|
/// Used as a fallback image when a look-up texture cannot be loaded.
|
|
private lazy var checkers: MTLTexture = {
|
|
// swiftlint:disable identifier_name force_try
|
|
let T0 = UInt32(0xff000000)
|
|
let T1 = UInt32(0xffffffff)
|
|
var checkerboard = [
|
|
T0, T1, T0, T1, T0, T1, T0, T1,
|
|
T1, T0, T1, T0, T1, T0, T1, T0,
|
|
T0, T1, T0, T1, T0, T1, T0, T1,
|
|
T1, T0, T1, T0, T1, T0, T1, T0,
|
|
T0, T1, T0, T1, T0, T1, T0, T1,
|
|
T1, T0, T1, T0, T1, T0, T1, T0,
|
|
T0, T1, T0, T1, T0, T1, T0, T1,
|
|
T1, T0, T1, T0, T1, T0, T1, T0,
|
|
]
|
|
|
|
let ctx = CGContext(data: &checkerboard,
|
|
width: 8,
|
|
height: 8,
|
|
bitsPerComponent: 8,
|
|
bytesPerRow: 32,
|
|
space: CGColorSpaceCreateDeviceRGB(),
|
|
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue)!
|
|
let img = ctx.makeImage()!
|
|
return try! loader.newTexture(cgImage: img)
|
|
}()
|
|
|
|
#if os(macOS)
|
|
// device behaviour
|
|
private let deviceHasUnifiedMemory: Bool
|
|
#endif
|
|
|
|
// parameters state
|
|
private var parameters = [Float](repeating: 0, count: Constants.maxParameters)
|
|
private var parametersCount = 0
|
|
private var parametersMap = [String: Int]()
|
|
|
|
public init(device: MTLDevice) throws {
|
|
self.device = device
|
|
#if os(macOS)
|
|
if #available(macOS 10.15, iOS 11, *) {
|
|
deviceHasUnifiedMemory = device.hasUnifiedMemory
|
|
} else {
|
|
deviceHasUnifiedMemory = false
|
|
}
|
|
#endif
|
|
|
|
library = try device.makeDefaultLibrary(bundle: Bundle(for: Self.self))
|
|
loader = .init(device: device)
|
|
pipelineState = try Self.makePipelineState(device, library)
|
|
samplers = try Self.makeSamplers(device)
|
|
vertexSizeBytes = MemoryLayout<Vertex>.stride * vertex.count
|
|
|
|
rotation = 0
|
|
setDefaultFilteringLinear(false)
|
|
}
|
|
|
|
// MARK: - Static helpers
|
|
|
|
private static func makePipelineState(_ device: MTLDevice, _ library: MTLLibrary) throws -> MTLRenderPipelineState {
|
|
let vd = MTLVertexDescriptor()
|
|
if let attr = vd.attributes[VertexAttribute.position.rawValue] {
|
|
attr.offset = MemoryLayout<Vertex>.offset(of: \.position)!
|
|
attr.format = .float4
|
|
attr.bufferIndex = BufferIndex.positions.rawValue
|
|
}
|
|
if let attr = vd.attributes[VertexAttribute.texCoord.rawValue] {
|
|
attr.offset = MemoryLayout<Vertex>.offset(of: \.texCoord)!
|
|
attr.format = .float2
|
|
attr.bufferIndex = BufferIndex.positions.rawValue
|
|
}
|
|
if let l = vd.layouts[BufferIndex.positions.rawValue] {
|
|
l.stride = MemoryLayout<Vertex>.stride
|
|
}
|
|
|
|
let psd = MTLRenderPipelineDescriptor()
|
|
psd.label = "Pipeline+No Alpha"
|
|
|
|
if let ca = psd.colorAttachments[0] {
|
|
ca.pixelFormat = .bgra8Unorm // NOTE(sgc): Required layer format (could be taken from layer.pixelFormat)
|
|
ca.isBlendingEnabled = false
|
|
ca.sourceAlphaBlendFactor = .sourceAlpha
|
|
ca.sourceRGBBlendFactor = .sourceAlpha
|
|
ca.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
|
ca.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
|
}
|
|
|
|
psd.vertexDescriptor = vd
|
|
psd.vertexFunction = library.makeFunction(name: "basic_vertex_proj_tex")
|
|
psd.fragmentFunction = library.makeFunction(name: "basic_fragment_proj_tex")
|
|
|
|
return try device.makeRenderPipelineState(descriptor: psd)
|
|
}
|
|
|
|
private static func makeSamplers(_ device: MTLDevice) throws -> SamplerFilterArray<MTLSamplerState> {
|
|
var samplers = [[MTLSamplerState?]](repeating: [MTLSamplerState?](repeating: nil,
|
|
count: ShaderPassWrap.allCases.count),
|
|
count: ShaderPassFilter.allCases.count)
|
|
let sd = MTLSamplerDescriptor()
|
|
for i in ShaderPassWrap.allCases {
|
|
var label = ""
|
|
switch i {
|
|
case .border:
|
|
if #available(macOS 10.15, iOS 11, *) {
|
|
if device.supportsFamily(.apple1) {
|
|
label = "clamp_to_zero"
|
|
sd.sAddressMode = .clampToZero
|
|
break
|
|
}
|
|
}
|
|
if #available(macOS 10.12, iOS 14, *) {
|
|
label = "clamp_to_border"
|
|
sd.sAddressMode = .clampToBorderColor
|
|
} else {
|
|
label = "clamp_to_zero"
|
|
sd.sAddressMode = .clampToZero
|
|
}
|
|
|
|
case .edge:
|
|
label = "clamp_to_edge"
|
|
sd.sAddressMode = .clampToEdge
|
|
|
|
case .repeat:
|
|
label = "repeat"
|
|
sd.sAddressMode = .repeat
|
|
|
|
case .mirroredRepeat:
|
|
label = "mirrored_repeat"
|
|
sd.sAddressMode = .mirrorRepeat
|
|
}
|
|
|
|
sd.tAddressMode = sd.sAddressMode
|
|
sd.rAddressMode = sd.sAddressMode
|
|
sd.minFilter = .linear
|
|
sd.magFilter = .linear
|
|
sd.label = "\(label) (linear)"
|
|
if let ss = device.makeSamplerState(descriptor: sd) {
|
|
samplers[.linear][i] = ss
|
|
} else {
|
|
throw InitError.invalidSamplerState
|
|
}
|
|
|
|
sd.minFilter = .nearest
|
|
sd.magFilter = .nearest
|
|
sd.label = label
|
|
if let ss = device.makeSamplerState(descriptor: sd) {
|
|
samplers[.nearest][i] = ss
|
|
} else {
|
|
throw InitError.invalidSamplerState
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable:next force_cast
|
|
return samplers.map { $0.compactMap { $0 } } as! SamplerFilterArray<MTLSamplerState>
|
|
}
|
|
|
|
public var rotation: Float {
|
|
get { _rotation }
|
|
set {
|
|
_rotation = newValue * 270
|
|
|
|
uniformsNoRotate.projectionMatrix = .makeOrtho(left: 0, right: 1, top: 0, bottom: 1)
|
|
|
|
let rot = simd_float4x4.makeRotated(z: (Float.pi * newValue) / 180.0)
|
|
uniforms.projectionMatrix = rot * uniformsNoRotate.projectionMatrix
|
|
}
|
|
}
|
|
|
|
/// Sets the default filtering mode when a shader pass leaves the value unspecified.
|
|
///
|
|
/// - parameters:
|
|
/// - linear: `true` to use linear filtering
|
|
public func setDefaultFilteringLinear(_ linear: Bool) {
|
|
if linear {
|
|
samplers[.unspecified] = samplers[.linear]
|
|
} else {
|
|
samplers[.unspecified] = samplers[.nearest]
|
|
}
|
|
}
|
|
|
|
private func updateHistory() {
|
|
guard historyCount > 0 else { return }
|
|
|
|
if historyNeedsInit {
|
|
initHistory()
|
|
} else {
|
|
// shift history and move last texture into first position
|
|
let tmp = historyTextures[historyCount]
|
|
for k in (1...historyCount).reversed() {
|
|
historyTextures[k] = historyTextures[k - 1]
|
|
}
|
|
historyTextures[0] = tmp
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Take the raw visible game rect and turn it into a smaller rect
|
|
* which is centered inside 'bounds' and has aspect ratio 'aspectSize'.
|
|
* Currently we try to fill the window, but maybe someday we'll support fixed zooms.
|
|
*/
|
|
private static func fitAspectRectIntoRect(aspectSize: CGSize, size: CGSize) -> CGRect {
|
|
let wantAspect = aspectSize.width / aspectSize.height
|
|
let viewAspect = size.width / size.height
|
|
|
|
var minFactor: CGFloat
|
|
var outRectSize: CGSize
|
|
|
|
if viewAspect >= wantAspect {
|
|
// Raw image is too wide (normal case), squish inwards
|
|
minFactor = wantAspect / viewAspect
|
|
outRectSize = .init(width: size.width * minFactor, height: size.height)
|
|
} else {
|
|
// Raw image is too tall, squish upwards
|
|
minFactor = viewAspect / wantAspect
|
|
outRectSize = .init(width: size.width, height: size.height * minFactor)
|
|
}
|
|
|
|
let outRect = CGRect(origin: .init(x: (size.width - outRectSize.width) / 2,
|
|
y: (size.height - outRectSize.height) / 2),
|
|
size: outRectSize)
|
|
|
|
// This is going into a Nearest Neighbor, so the edges should be on pixels!
|
|
return outRect.integral
|
|
}
|
|
|
|
private func resize() {
|
|
let bounds = Self.fitAspectRectIntoRect(aspectSize: aspectSize, size: drawableSize)
|
|
if outputBounds == bounds {
|
|
return
|
|
}
|
|
|
|
outputBounds = bounds
|
|
let size = outputBounds.size
|
|
|
|
outputFrame.viewport = MTLViewport(originX: outputBounds.origin.x,
|
|
originY: outputBounds.origin.y,
|
|
width: size.width,
|
|
height: size.height,
|
|
znear: 0,
|
|
zfar: 1)
|
|
outputFrame.outputSize = .init(width: size.width, height: size.height)
|
|
|
|
if hasShader {
|
|
renderTargetsNeedResize = true
|
|
}
|
|
}
|
|
|
|
public func setSourceRect(_ rect: CGRect, aspect: CGSize) {
|
|
if sourceRect == rect, aspectSize == aspect {
|
|
return
|
|
}
|
|
|
|
sourceRect = rect
|
|
aspectSize = aspect
|
|
resize()
|
|
}
|
|
|
|
public var drawableSize: CGSize = .zero {
|
|
didSet {
|
|
resize()
|
|
}
|
|
}
|
|
|
|
/// A list of textures to be cleared before rendering begins.
|
|
var _clearTextures = [MTLTexture]()
|
|
|
|
private func clearTexturesWithCommandBuffer(_ commandBuffer: MTLCommandBuffer) {
|
|
guard !_clearTextures.isEmpty else { return }
|
|
|
|
defer { _clearTextures.removeAll(keepingCapacity: true) }
|
|
|
|
guard #available(macOS 10.15, *) else { return }
|
|
|
|
// Find the size of the largest texture, in order to allocate a buffer with at least enough space for all textures.
|
|
var sizeMax = 0
|
|
for t in _clearTextures {
|
|
let bytesPerPixel = t.pixelFormat.bytesPerPixel
|
|
precondition(bytesPerPixel > 0, "Unable to determine bytes per pixel for pixel format \(t.pixelFormat)")
|
|
|
|
let bytesPerRow = t.width * bytesPerPixel
|
|
let bytesPerImage = t.height * bytesPerRow
|
|
if bytesPerImage > sizeMax {
|
|
sizeMax = bytesPerImage
|
|
}
|
|
}
|
|
|
|
// Allocate a buffer over the entire heap and fill it with zeros
|
|
if let bce = commandBuffer.makeBlitCommandEncoder(),
|
|
let buf = device.makeBuffer(length: sizeMax, options: [.storageModePrivate])
|
|
{
|
|
bce.fill(buffer: buf, range: 0..<sizeMax, value: 0)
|
|
|
|
// Use the cleared buffer to clear the destination texture.
|
|
for t in _clearTextures {
|
|
let bytesPerPixel = t.pixelFormat.bytesPerPixel
|
|
let bytesPerRow = t.width * bytesPerPixel
|
|
let bytesPerImage = t.height * bytesPerRow
|
|
let sourceSize = MTLSize(width: t.width, height: t.height, depth: 1)
|
|
bce.copy(from: buf, sourceOffset: 0, sourceBytesPerRow: bytesPerRow, sourceBytesPerImage: bytesPerImage, sourceSize: sourceSize,
|
|
to: t, destinationSlice: 0, destinationLevel: 0, destinationOrigin: .init())
|
|
}
|
|
bce.endEncoding()
|
|
}
|
|
}
|
|
|
|
private func fetchNextHistoryTexture() -> MTLTexture {
|
|
precondition(historyCount > 0, "Current shader does not require history")
|
|
|
|
// either no history, or we moved a texture of a different size in the front slot
|
|
if historyTextures[0].size.x != Float(sourceRect.width) || historyTextures[0].size.y != Float(sourceRect.height) {
|
|
let td = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm,
|
|
width: Int(sourceRect.width),
|
|
height: Int(sourceRect.height),
|
|
mipmapped: false)
|
|
td.storageMode = .private
|
|
td.usage = [.shaderRead, .shaderWrite]
|
|
initTexture(&historyTextures[0], withDescriptor: td)
|
|
}
|
|
|
|
return historyTextures[0].view!
|
|
}
|
|
|
|
private func prepareNextFrame(sourceTexture: MTLTexture, commandBuffer: MTLCommandBuffer) {
|
|
frameCount += 1
|
|
|
|
resizeRenderTargets()
|
|
updateHistory()
|
|
clearTexturesWithCommandBuffer(commandBuffer)
|
|
|
|
if historyCount == 0 {
|
|
// No need to copy, set the sourceTexture to Original / OriginalHistory0 semantic
|
|
initTexture(&historyTextures[0], withTexture: sourceTexture)
|
|
} else {
|
|
let texture = fetchNextHistoryTexture()
|
|
|
|
let orig = MTLOrigin(x: Int(sourceRect.origin.x), y: Int(sourceRect.origin.y), z: 0)
|
|
let size = MTLSize(width: Int(sourceRect.width), height: Int(sourceRect.height), depth: 1)
|
|
let zero = MTLOrigin()
|
|
|
|
if let bce = commandBuffer.makeBlitCommandEncoder() {
|
|
bce.copy(from: sourceTexture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: orig, sourceSize: size,
|
|
to: texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: zero)
|
|
bce.endEncoding()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func initTexture(_ t: inout Texture, withDescriptor td: MTLTextureDescriptor) {
|
|
t.view = device.makeTexture(descriptor: td)
|
|
t.size = .init(width: td.width, height: td.height)
|
|
}
|
|
|
|
private func initTexture(_ t: inout Texture, withTexture tex: MTLTexture) {
|
|
t.view = tex
|
|
t.size = .init(width: tex.width, height: tex.height)
|
|
}
|
|
|
|
private func initHistory() {
|
|
let td = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm,
|
|
width: Int(sourceRect.width),
|
|
height: Int(sourceRect.height),
|
|
mipmapped: false)
|
|
td.storageMode = .private
|
|
td.usage = [.shaderRead, .shaderWrite]
|
|
|
|
for i in 0...historyCount {
|
|
initTexture(&historyTextures[i], withDescriptor: td)
|
|
_clearTextures.append(historyTextures[i].view!)
|
|
}
|
|
historyNeedsInit = false
|
|
}
|
|
|
|
private func renderTexture(_ texture: MTLTexture, renderCommandEncoder rce: MTLRenderCommandEncoder) {
|
|
rce.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: BufferIndex.uniforms.rawValue)
|
|
rce.setRenderPipelineState(pipelineState)
|
|
rce.setFragmentSamplerState(samplers[.nearest][.edge], index: SamplerIndex.draw.rawValue)
|
|
rce.setViewport(outputFrame.viewport)
|
|
rce.setFragmentTexture(texture, index: TextureIndex.color.rawValue)
|
|
rce.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
|
}
|
|
|
|
public func render(sourceTexture: MTLTexture,
|
|
commandBuffer: MTLCommandBuffer,
|
|
renderPassDescriptor rpd: MTLRenderPassDescriptor,
|
|
flipVertically: Bool = false)
|
|
{
|
|
renderOffscreenPasses(sourceTexture: sourceTexture, commandBuffer: commandBuffer)
|
|
if let rce = commandBuffer.makeRenderCommandEncoder(descriptor: rpd) {
|
|
renderFinalPass(withCommandEncoder: rce, flipVertically: flipVertically)
|
|
rce.endEncoding()
|
|
}
|
|
}
|
|
|
|
public func renderOffscreenPasses(sourceTexture: MTLTexture, commandBuffer: MTLCommandBuffer) {
|
|
prepareNextFrame(sourceTexture: sourceTexture, commandBuffer: commandBuffer)
|
|
updateBuffersForPasses()
|
|
|
|
guard hasShader, passCount > 0 else { return }
|
|
|
|
// swap feedback render targets
|
|
for i in 0..<passCount where pass[i].hasFeedback {
|
|
(pass[i].renderTarget, pass[i].feedbackTarget) = (pass[i].feedbackTarget, pass[i].renderTarget)
|
|
}
|
|
|
|
let rpd = MTLRenderPassDescriptor()
|
|
rpd.colorAttachments[0].loadAction = .dontCare
|
|
rpd.colorAttachments[0].storeAction = .store
|
|
let lassPassIsDirect = pass[lastPassIndex].renderTarget.view == nil
|
|
let count = lassPassIsDirect ? passCount - 1 : passCount
|
|
|
|
for i in 0..<count {
|
|
rpd.colorAttachments[0].texture = pass[i].renderTarget.view
|
|
guard let rce = commandBuffer.makeRenderCommandEncoder(descriptor: rpd) else { continue }
|
|
rce.setVertexBytes(&vertex, length: vertexSizeBytes, index: BufferIndex.positions.rawValue)
|
|
rce.setViewport(pass[i].viewport)
|
|
renderPassIndex(i, renderCommandEncoder: rce)
|
|
rce.endEncoding()
|
|
}
|
|
}
|
|
|
|
private func updateBuffersForPasses() {
|
|
for i in 0..<passCount {
|
|
pass[i].frameDirection = Int32(frameDirection)
|
|
pass[i].frameCount = UInt32(frameCount)
|
|
if pass[i].frameCountMod != 0 {
|
|
pass[i].frameCount %= pass[i].frameCountMod
|
|
}
|
|
|
|
for j in 0..<Constants.maxConstantBuffers {
|
|
let sem = pass[i].bindings!.buffers[j]
|
|
|
|
guard
|
|
sem.bindingVert != nil || sem.bindingFrag != nil,
|
|
!sem.uniforms.isEmpty
|
|
else { continue }
|
|
|
|
if let buffer = pass[i].buffers[j] {
|
|
let data = buffer.contents()
|
|
for uniform in sem.uniforms {
|
|
data.advanced(by: uniform.offset).copyMemory(from: uniform.data, byteCount: uniform.size)
|
|
}
|
|
#if os(macOS)
|
|
if !deviceHasUnifiedMemory {
|
|
buffer.didModifyRange(0..<buffer.length)
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// these fields are used as temporary storage when rendering each pass
|
|
private var _renderTextures: [MTLTexture?] = .init(repeating: nil, count: Constants.maxShaderBindings)
|
|
private var _renderSamplers: [MTLSamplerState?] = .init(repeating: nil, count: Constants.maxShaderBindings)
|
|
private var _renderbOffsets: [Int] = .init(repeating: 0, count: Constants.maxConstantBuffers)
|
|
|
|
private func renderPassIndex(_ i: Int, renderCommandEncoder rce: MTLRenderCommandEncoder) {
|
|
defer {
|
|
// clear references from temporary buffers
|
|
for i in 0..<Constants.maxShaderBindings {
|
|
_renderTextures[i] = nil
|
|
_renderSamplers[i] = nil
|
|
}
|
|
}
|
|
|
|
for bind in pass[i].bindings!.textures {
|
|
let binding = Int(bind.binding)
|
|
_renderTextures[binding] = bind.texture.load(as: MTLTexture?.self)
|
|
_renderSamplers[binding] = samplers[bind.filter][bind.wrap]
|
|
}
|
|
|
|
// enqueue commands
|
|
rce.setRenderPipelineState(pass[i].state!)
|
|
rce.label = pass[i].state!.label
|
|
|
|
rce.setVertexBuffers(pass[i].vBuffers, offsets: _renderbOffsets, range: 0..<Constants.maxConstantBuffers)
|
|
rce.setFragmentBuffers(pass[i].fBuffers, offsets: _renderbOffsets, range: 0..<Constants.maxConstantBuffers)
|
|
rce.setFragmentTextures(_renderTextures, range: 0..<Constants.maxShaderBindings)
|
|
rce.setFragmentSamplerStates(_renderSamplers, range: 0..<Constants.maxShaderBindings)
|
|
rce.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
|
}
|
|
|
|
public func renderFinalPass(withCommandEncoder rce: MTLRenderCommandEncoder, flipVertically: Bool) {
|
|
rce.setViewport(outputFrame.viewport)
|
|
if flipVertically {
|
|
rce.setVertexBytes(&vertexFlipped, length: vertexSizeBytes, index: BufferIndex.positions.rawValue)
|
|
} else {
|
|
rce.setVertexBytes(&vertex, length: vertexSizeBytes, index: BufferIndex.positions.rawValue)
|
|
}
|
|
|
|
if !hasShader || passCount == 0 {
|
|
guard let texture = historyTextures[0].view else { return }
|
|
return renderTexture(texture, renderCommandEncoder: rce)
|
|
}
|
|
|
|
if let view = pass[lastPassIndex].renderTarget.view {
|
|
return renderTexture(view, renderCommandEncoder: rce)
|
|
} else {
|
|
// last pass renders directly to the final render target
|
|
return renderPassIndex(lastPassIndex, renderCommandEncoder: rce)
|
|
}
|
|
}
|
|
|
|
private func resizeRenderTargets() {
|
|
guard renderTargetsNeedResize else { return }
|
|
|
|
// current source size
|
|
var sourceSize = sourceRect.size
|
|
|
|
let viewportSize = CGSize(width: outputFrame.viewport.width, height: outputFrame.viewport.height)
|
|
|
|
for i in 0..<passCount {
|
|
let pass = pass[i]
|
|
|
|
let passSize: CGSize
|
|
if !pass.isScaled {
|
|
passSize = i == lastPassIndex ? viewportSize : sourceSize
|
|
} else {
|
|
passSize = pass.getOutputSize(viewport: viewportSize, source: sourceSize)
|
|
}
|
|
|
|
sourceSize = passSize // capture source size for next pass
|
|
|
|
os_log("pass %d, render target size %0.0f x %0.0f", log: .default, type: .debug, i, passSize.width, passSize.height)
|
|
|
|
let fmt = self.pass[i].format
|
|
if i == lastPassIndex, passSize == viewportSize, fmt == .bgra8Unorm {
|
|
// last pass can render directly to the output render target
|
|
self.pass[i].renderTarget.size = .init(width: passSize.width, height: passSize.height)
|
|
} else {
|
|
let (width, height) = (Int(passSize.width), Int(passSize.height))
|
|
|
|
self.pass[i].viewport = .init(originX: 0, originY: 0,
|
|
width: Double(width), height: Double(height),
|
|
znear: 0, zfar: 1)
|
|
|
|
if let tex = self.pass[i].renderTarget.view,
|
|
tex.width == width,
|
|
tex.height == height,
|
|
tex.width != 0, tex.height != 0
|
|
{
|
|
os_log("pass %d: 🏎️🔥 skip resize, tex (w: %d, h: %d) == pass (w: %d, h: %d)",
|
|
log: .default, type: .debug,
|
|
i, tex.width, tex.height, width, height)
|
|
let size = TextureSize(width: width, height: height)
|
|
self.pass[i].renderTarget.size = size
|
|
if self.pass[i].hasFeedback {
|
|
self.pass[i].feedbackTarget.size = size
|
|
}
|
|
continue
|
|
}
|
|
let td = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: fmt,
|
|
width: width,
|
|
height: height,
|
|
mipmapped: false)
|
|
td.storageMode = .private
|
|
td.usage = [.shaderRead, .renderTarget]
|
|
initTexture(&self.pass[i].renderTarget, withDescriptor: td)
|
|
// textures should be cleared before first use
|
|
_clearTextures.append(self.pass[i].renderTarget.view!)
|
|
|
|
let label = String(format: "Pass %02d Output", i)
|
|
self.pass[i].renderTarget.view?.label = label
|
|
if self.pass[i].hasFeedback {
|
|
initTexture(&self.pass[i].feedbackTarget, withDescriptor: td)
|
|
self.pass[i].feedbackTarget.view?.label = label
|
|
_clearTextures.append(self.pass[i].feedbackTarget.view!)
|
|
}
|
|
}
|
|
}
|
|
|
|
renderTargetsNeedResize = false
|
|
}
|
|
|
|
private func freeShaderResources() {
|
|
pass = .init(repeating: .init(), count: Constants.maxShaderPasses)
|
|
luts = .init(repeating: .init(), count: Constants.maxTextures)
|
|
historyTextures = .init(repeating: .init(), count: Constants.maxFrameHistory + 1)
|
|
|
|
parameters = .init(repeating: 0, count: Constants.maxParameters)
|
|
parametersMap = [:]
|
|
parametersCount = 0
|
|
|
|
historyCount = 0
|
|
passCount = 0
|
|
lastPassIndex = 0
|
|
hasShader = false
|
|
}
|
|
|
|
public func setCompiledShader(_ container: CompiledShaderContainer) throws {
|
|
freeShaderResources()
|
|
|
|
let start = CACurrentMediaTime()
|
|
|
|
let ss = container.shader
|
|
|
|
passCount = ss.passes.count
|
|
lastPassIndex = passCount - 1
|
|
|
|
parametersCount = ss.parameters.count
|
|
parametersMap = .init(uniqueKeysWithValues: ss.parameters.enumerated().map { index, param in (param.name, index) })
|
|
parameters = .init(ss.parameters.map { ($0.initial as NSDecimalNumber).floatValue })
|
|
|
|
let texStride = MemoryLayout<Texture>.stride
|
|
let texViewOffset = MemoryLayout<Texture>.offset(of: \.view)!
|
|
let texSizeOffset = MemoryLayout<Texture>.offset(of: \.size)!
|
|
|
|
for passNumber in 0..<passCount {
|
|
let sem = ShaderPassSemantics()
|
|
|
|
withUnsafePointer(to: &historyTextures[0]) {
|
|
let p = UnsafeRawPointer($0)
|
|
sem.addTexture(p.advanced(by: texViewOffset),
|
|
size: p.advanced(by: texSizeOffset),
|
|
semantic: .original)
|
|
|
|
sem.addTexture(p.advanced(by: texViewOffset), stride: texStride,
|
|
size: p.advanced(by: texSizeOffset), stride: texStride,
|
|
semantic: .originalHistory)
|
|
|
|
if passNumber == 0 {
|
|
// The source texture for first pass is the original input texture
|
|
sem.addTexture(p.advanced(by: texViewOffset),
|
|
size: p.advanced(by: texSizeOffset),
|
|
semantic: .source)
|
|
}
|
|
}
|
|
|
|
if passNumber > 0 {
|
|
// The source texture for passes 1..<n is the output of the previous pass
|
|
withUnsafePointer(to: &pass[passNumber - 1].renderTarget) {
|
|
let p = UnsafeRawPointer($0)
|
|
sem.addTexture(p.advanced(by: texViewOffset),
|
|
size: p.advanced(by: texSizeOffset),
|
|
semantic: .source)
|
|
}
|
|
}
|
|
|
|
withUnsafePointer(to: &pass[0]) {
|
|
let p = UnsafeRawPointer($0)
|
|
|
|
let rt = p.advanced(by: MemoryLayout<Pass>.offset(of: \.renderTarget)!)
|
|
sem.addTexture(rt.advanced(by: texViewOffset), stride: MemoryLayout<Pass>.stride,
|
|
size: rt.advanced(by: texSizeOffset), stride: MemoryLayout<Pass>.stride,
|
|
semantic: .passOutput)
|
|
|
|
let ft = p.advanced(by: MemoryLayout<Pass>.offset(of: \.feedbackTarget)!)
|
|
sem.addTexture(ft.advanced(by: texViewOffset), stride: MemoryLayout<Pass>.stride,
|
|
size: ft.advanced(by: texSizeOffset), stride: MemoryLayout<Pass>.stride,
|
|
semantic: .passFeedback)
|
|
}
|
|
|
|
withUnsafePointer(to: &luts[0]) {
|
|
let p = UnsafeRawPointer($0)
|
|
sem.addTexture(p.advanced(by: texViewOffset), stride: texStride,
|
|
size: p.advanced(by: texSizeOffset), stride: texStride,
|
|
semantic: .user)
|
|
}
|
|
|
|
if passNumber == lastPassIndex {
|
|
withUnsafePointer(to: &uniforms.projectionMatrix) {
|
|
sem.addUniformData(UnsafeRawPointer($0), semantic: .mvp)
|
|
}
|
|
} else {
|
|
withUnsafePointer(to: &uniformsNoRotate.projectionMatrix) {
|
|
sem.addUniformData(UnsafeRawPointer($0), semantic: .mvp)
|
|
}
|
|
}
|
|
|
|
withUnsafePointer(to: &pass[passNumber]) {
|
|
let p = UnsafeRawPointer($0)
|
|
sem.addUniformData(p.advanced(by: MemoryLayout<Pass>.offset(of: \.renderTarget.size)!), semantic: .outputSize)
|
|
sem.addUniformData(p.advanced(by: MemoryLayout<Pass>.offset(of: \.frameCount)!), semantic: .frameCount)
|
|
sem.addUniformData(p.advanced(by: MemoryLayout<Pass>.offset(of: \.frameDirection)!), semantic: .frameDirection)
|
|
}
|
|
|
|
withUnsafePointer(to: &outputFrame.outputSize) {
|
|
sem.addUniformData(UnsafeRawPointer($0), semantic: .finalViewportSize)
|
|
}
|
|
|
|
for i in 0..<parametersCount {
|
|
withUnsafePointer(to: ¶meters[i]) {
|
|
sem.addUniformData(UnsafeRawPointer($0), forParameterAt: i)
|
|
}
|
|
}
|
|
|
|
let bindings = ShaderPassBindings()
|
|
let pass = ss.passes[passNumber]
|
|
updateBindings(passBindings: bindings,
|
|
forPassNumber: passNumber,
|
|
passSemantics: sem,
|
|
pass: pass)
|
|
self.pass[passNumber].bindings = bindings
|
|
self.pass[passNumber].format = .init(pass.format)
|
|
self.pass[passNumber].frameCountMod = UInt32(pass.frameCountMod)
|
|
|
|
// update scaling
|
|
self.pass[passNumber].scaleX = .init(pass.scaleX)
|
|
self.pass[passNumber].scaleY = .init(pass.scaleY)
|
|
|
|
let vd = MTLVertexDescriptor()
|
|
if let attr = vd.attributes[VertexAttribute.position.rawValue] {
|
|
attr.offset = MemoryLayout<Vertex>.offset(of: \.position)!
|
|
attr.format = .float4
|
|
attr.bufferIndex = BufferIndex.positions.rawValue
|
|
}
|
|
if let attr = vd.attributes[VertexAttribute.texCoord.rawValue] {
|
|
attr.offset = MemoryLayout<Vertex>.offset(of: \.texCoord)!
|
|
attr.format = .float2
|
|
attr.bufferIndex = BufferIndex.positions.rawValue
|
|
}
|
|
if let l = vd.layouts[BufferIndex.positions.rawValue] {
|
|
l.stride = MemoryLayout<Vertex>.stride
|
|
}
|
|
|
|
let psd = MTLRenderPipelineDescriptor()
|
|
if let alias = pass.alias {
|
|
psd.label = "pass \(passNumber) (\(alias))"
|
|
} else {
|
|
psd.label = "pass \(passNumber)"
|
|
}
|
|
|
|
if let ca = psd.colorAttachments[0] {
|
|
ca.pixelFormat = self.pass[passNumber].format
|
|
ca.isBlendingEnabled = false
|
|
ca.sourceAlphaBlendFactor = .sourceAlpha
|
|
ca.sourceRGBBlendFactor = .sourceAlpha
|
|
ca.destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
|
ca.destinationRGBBlendFactor = .oneMinusSourceAlpha
|
|
}
|
|
|
|
psd.sampleCount = 1
|
|
psd.vertexDescriptor = vd
|
|
|
|
let options = MTLCompileOptions()
|
|
options.languageVersion = try MTLLanguageVersion(ss.languageVersion)
|
|
do {
|
|
let lib = try device.makeLibrary(source: pass.vertexSource, options: options)
|
|
psd.vertexFunction = lib.makeFunction(name: "main0")
|
|
}
|
|
|
|
do {
|
|
let lib = try device.makeLibrary(source: pass.fragmentSource, options: options)
|
|
psd.fragmentFunction = lib.makeFunction(name: "main0")
|
|
}
|
|
|
|
self.pass[passNumber].state = try device.makeRenderPipelineState(descriptor: psd)
|
|
|
|
for j in 0..<Constants.maxConstantBuffers {
|
|
let sem = self.pass[passNumber].bindings!.buffers[j]
|
|
|
|
let size = sem.size
|
|
guard size > 0 else { continue }
|
|
#if os(macOS)
|
|
let opts: MTLResourceOptions = deviceHasUnifiedMemory ? .storageModeShared : .storageModeManaged
|
|
#else
|
|
let opts: MTLResourceOptions = .storageModeShared
|
|
#endif
|
|
let buf = device.makeBuffer(length: size, options: opts)
|
|
self.pass[passNumber].buffers[j] = buf
|
|
|
|
if let binding = sem.bindingVert {
|
|
self.pass[passNumber].vBuffers[binding] = buf
|
|
}
|
|
if let binding = sem.bindingFrag {
|
|
self.pass[passNumber].fBuffers[binding] = buf
|
|
}
|
|
}
|
|
}
|
|
|
|
// finalise remaining state
|
|
historyCount = ss.historyCount
|
|
for pass in ss.passes {
|
|
self.pass[pass.index].hasFeedback = pass.isFeedback
|
|
}
|
|
|
|
let end = CACurrentMediaTime() - start
|
|
os_log("Shader load completed in %{xcode:interval}f seconds", log: .default, type: .debug, end)
|
|
|
|
loadLuts(container)
|
|
hasShader = true
|
|
renderTargetsNeedResize = true
|
|
historyNeedsInit = true
|
|
}
|
|
|
|
private func loadLuts(_ cc: CompiledShaderContainer) {
|
|
let opts: [MTKTextureLoader.Option: Any] = [
|
|
.generateMipmaps: true,
|
|
.allocateMipmaps: true,
|
|
.SRGB: false,
|
|
.textureStorageMode: MTLStorageMode.private.rawValue,
|
|
]
|
|
|
|
let images = cc.shader.luts
|
|
|
|
var i = 0
|
|
for lut in images {
|
|
let t: MTLTexture
|
|
do {
|
|
let data = try cc.getLUTByName(lut.name)
|
|
if let cgImg = CGContext.makeForTexture(data: data)?.makeImage() {
|
|
t = try loader.newTexture(cgImage: cgImg, options: opts)
|
|
} else {
|
|
t = checkers
|
|
}
|
|
} catch {
|
|
os_log("Unable to load LUT texture, using default. Path '%{public}@: %{public}@", log: .default, type: .error,
|
|
lut.url.absoluteString, error.localizedDescription)
|
|
t = checkers
|
|
}
|
|
initTexture(&luts[i], withTexture: t)
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
func updateBindings(passBindings: ShaderPassBindings, forPassNumber passNumber: Int, passSemantics: ShaderPassSemantics, pass: Compiled.ShaderPass) {
|
|
func addUniforms(bufferIndex: Int) {
|
|
let desc = pass.buffers[bufferIndex]
|
|
guard desc.size > 0 else { return }
|
|
|
|
let bind = passBindings.buffers[bufferIndex]
|
|
bind.bindingVert = desc.bindingVert
|
|
bind.bindingFrag = desc.bindingFrag
|
|
bind.size = (desc.size + 0xf) & ~0xf // round up to nearest 16 bytes
|
|
|
|
for u in desc.uniforms {
|
|
switch u.semantic {
|
|
case .floatParameter:
|
|
guard let param = passSemantics.parameter(at: u.index!)
|
|
else { fatalError("Unable to find parameter at index \(u.index!)") }
|
|
bind.addUniformData(param.data,
|
|
size: u.size,
|
|
offset: u.offset,
|
|
name: u.name)
|
|
|
|
case .mvp, .outputSize, .finalViewportSize, .frameCount, .frameDirection:
|
|
bind.addUniformData(passSemantics.uniforms[u.semantic]!.data,
|
|
size: u.size,
|
|
offset: u.offset,
|
|
name: u.name)
|
|
|
|
case .originalSize, .sourceSize, .originalHistorySize, .passOutputSize, .passFeedbackSize, .userSize:
|
|
let tex = passSemantics.textureUniforms[u.semantic]!
|
|
|
|
bind.addUniformData(tex.size.advanced(by: u.index! * tex.stride),
|
|
size: u.size,
|
|
offset: u.offset,
|
|
name: u.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UBO
|
|
addUniforms(bufferIndex: 0)
|
|
// Push
|
|
addUniforms(bufferIndex: 1)
|
|
|
|
for t in pass.textures {
|
|
let tex = passSemantics.textures[t.semantic]!
|
|
let bind = passBindings.addTexture(tex.texture.advanced(by: t.index * tex.stride),
|
|
binding: t.binding,
|
|
name: t.name)
|
|
bind.wrap = .init(t.wrap)
|
|
bind.filter = .init(t.filter)
|
|
}
|
|
}
|
|
|
|
public func setValue(_ value: CGFloat, forParameterName name: String) {
|
|
if let index = parametersMap[name] {
|
|
parameters[index] = Float(value)
|
|
}
|
|
}
|
|
|
|
public func setValue(_ value: CGFloat, forParameterIndex index: Int) {
|
|
if case 0..<parametersCount = index {
|
|
parameters[index] = Float(value)
|
|
}
|
|
}
|
|
|
|
typealias SamplerWrapArray<Element> = [Element]
|
|
typealias SamplerFilterArray<Element> = [SamplerWrapArray<Element>]
|
|
}
|
|
|
|
extension FilterChain.SamplerWrapArray {
|
|
subscript(x: ShaderPassWrap) -> Element {
|
|
get { self[x.rawValue] }
|
|
set { self[x.rawValue] = newValue }
|
|
}
|
|
}
|
|
|
|
extension FilterChain.SamplerFilterArray {
|
|
subscript(x: ShaderPassFilter) -> Element {
|
|
get { self[x.rawValue] }
|
|
set { self[x.rawValue] = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - Types shared between Metal shaders and Swift
|
|
|
|
extension FilterChain {
|
|
enum BufferIndex: Int {
|
|
case uniforms = 1
|
|
case positions = 4
|
|
}
|
|
|
|
enum VertexAttribute: Int {
|
|
case position = 0
|
|
case texCoord = 1
|
|
}
|
|
|
|
enum TextureIndex: Int {
|
|
case color = 0
|
|
}
|
|
|
|
enum SamplerIndex: Int {
|
|
case draw = 0
|
|
}
|
|
|
|
@frozen @usableFromInline struct Vertex {
|
|
let position: simd_float4
|
|
let texCoord: simd_float2
|
|
}
|
|
|
|
@frozen @usableFromInline struct Uniforms {
|
|
static let empty: Uniforms = .init(projectionMatrix: simd_float4x4(), outputSize: simd_float2(), time: 0)
|
|
|
|
var projectionMatrix: simd_float4x4
|
|
var outputSize: simd_float2
|
|
var time: simd_float1
|
|
}
|
|
}
|
|
|
|
private typealias TextureSize = SIMD4<Float>
|
|
|
|
extension TextureSize {
|
|
static let zero: Self = .init(x: 0, y: 0, z: 0, w: 0)
|
|
|
|
init(width w: Int, height h: Int) {
|
|
self.init(width: CGFloat(w), height: CGFloat(h))
|
|
}
|
|
|
|
init(width w: CGFloat, height h: CGFloat) {
|
|
let width = Float(w)
|
|
let height = Float(h)
|
|
self = .init(x: width, y: height, z: 1.0 / width, w: 1.0 / height)
|
|
}
|
|
}
|
|
|
|
private struct Texture {
|
|
var view: MTLTexture?
|
|
var size: TextureSize = .zero
|
|
}
|
|
|
|
public enum MTLLangageVersionError: LocalizedError {
|
|
case versionUnavailable
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .versionUnavailable:
|
|
return "The specified Metal version is unavailable for this OS or version of OpenEmuShaders"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MTLLanguageVersion {
|
|
init(_ languageVersion: Compiled.LanguageVersion) throws {
|
|
switch languageVersion {
|
|
case .version2_4:
|
|
if #available(macOS 12.0, iOS 15.0, *) {
|
|
self = .version2_4
|
|
} else {
|
|
throw MTLLangageVersionError.versionUnavailable
|
|
}
|
|
case .version2_3:
|
|
if #available(macOS 11.0, iOS 14.0, *) {
|
|
self = .version2_3
|
|
} else {
|
|
throw MTLLangageVersionError.versionUnavailable
|
|
}
|
|
case .version2_2:
|
|
if #available(macOS 10.15, iOS 13.0, *) {
|
|
self = .version2_2
|
|
} else {
|
|
throw MTLLangageVersionError.versionUnavailable
|
|
}
|
|
case .version2_1:
|
|
self = .version2_1
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MTLPixelFormat {
|
|
// swiftlint:disable cyclomatic_complexity
|
|
init(_ pixelFormat: Compiled.PixelFormat) {
|
|
switch pixelFormat {
|
|
case .r8Unorm:
|
|
self = .r8Unorm
|
|
case .r8Uint:
|
|
self = .r8Uint
|
|
case .r8Sint:
|
|
self = .r8Sint
|
|
case .rg8Unorm:
|
|
self = .rg8Unorm
|
|
case .rg8Uint:
|
|
self = .rg8Uint
|
|
case .rg8Sint:
|
|
self = .rg8Sint
|
|
case .rgba8Unorm:
|
|
self = .rgba8Unorm
|
|
case .rgba8Uint:
|
|
self = .rgba8Uint
|
|
case .rgba8Sint:
|
|
self = .rgba8Sint
|
|
case .rgba8Unorm_srgb:
|
|
self = .rgba8Unorm_srgb
|
|
case .rgb10a2Unorm:
|
|
self = .rgb10a2Unorm
|
|
case .rgb10a2Uint:
|
|
self = .rgb10a2Uint
|
|
case .r16Uint:
|
|
self = .r16Uint
|
|
case .r16Sint:
|
|
self = .r16Sint
|
|
case .r16Float:
|
|
self = .r16Float
|
|
case .rg16Uint:
|
|
self = .rg16Uint
|
|
case .rg16Sint:
|
|
self = .rg16Sint
|
|
case .rg16Float:
|
|
self = .rg16Float
|
|
case .rgba16Uint:
|
|
self = .rgba16Uint
|
|
case .rgba16Sint:
|
|
self = .rgba16Sint
|
|
case .rgba16Float:
|
|
self = .rgba16Float
|
|
case .r32Uint:
|
|
self = .r32Uint
|
|
case .r32Sint:
|
|
self = .r32Sint
|
|
case .r32Float:
|
|
self = .r32Float
|
|
case .rg32Uint:
|
|
self = .rg32Uint
|
|
case .rg32Sint:
|
|
self = .rg32Sint
|
|
case .rg32Float:
|
|
self = .rg32Float
|
|
case .rgba32Uint:
|
|
self = .rgba32Uint
|
|
case .rgba32Sint:
|
|
self = .rgba32Sint
|
|
case .rgba32Float:
|
|
self = .rgba32Float
|
|
case .bgra8Unorm_srgb:
|
|
self = .bgra8Unorm_srgb
|
|
case .bgra8Unorm:
|
|
self = .bgra8Unorm
|
|
}
|
|
}
|
|
|
|
/// Returns the number of bytes per pixel for the given format; otherwise, 0 if the format is not supported
|
|
var bytesPerPixel: Int {
|
|
switch self {
|
|
case .a8Unorm, .r8Unorm, .r8Unorm_srgb, .r8Snorm, .r8Uint, .r8Sint:
|
|
return 1
|
|
|
|
case .r16Unorm, .r16Snorm, .r16Uint, .r16Sint, .r16Float:
|
|
return 2
|
|
|
|
case .rg8Unorm, .rg8Unorm_srgb, .rg8Snorm, .rg8Uint, .rg8Sint, .b5g6r5Unorm, .a1bgr5Unorm, .abgr4Unorm,
|
|
.bgr5A1Unorm:
|
|
return 2
|
|
|
|
case .r32Uint, .r32Sint, .r32Float, .rg16Unorm, .rg16Snorm, .rg16Uint, .rg16Sint, .rg16Float, .rgba8Unorm,
|
|
.rgba8Unorm_srgb, .rgba8Snorm, .rgba8Uint, .rgba8Sint, .bgra8Unorm, .bgra8Unorm_srgb, .rgb10a2Unorm,
|
|
.rgb10a2Uint, .rg11b10Float, .rgb9e5Float, .bgr10a2Unorm, .bgr10_xr, .bgr10_xr_srgb:
|
|
return 4
|
|
|
|
case .rg32Uint, .rg32Sint, .rg32Float, .rgba16Unorm, .rgba16Snorm, .rgba16Uint, .rgba16Sint, .rgba16Float,
|
|
.bgra10_xr, .bgra10_xr_srgb:
|
|
return 8
|
|
|
|
case .rgba32Uint, .rgba32Sint, .rgba32Float:
|
|
return 16
|
|
|
|
case .invalid:
|
|
return 0
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CGContext {
|
|
/// Returns a context using the dimensions and contents from the image identified by the data.
|
|
/// The context data format is compatible with BGRA8
|
|
/// - Parameter data: The data of the source image.
|
|
/// - Returns: a new ``CGContext`` with dimensions and contents matching the source image.
|
|
static func makeForTexture(data: Data) -> CGContext? {
|
|
guard
|
|
let src = CGImageSourceCreateWithData(data as CFData, nil),
|
|
let img = CGImageSourceCreateImageAtIndex(src, CGImageSourceGetPrimaryImageIndex(src), nil)
|
|
else { return nil }
|
|
|
|
guard let context = CGContext(data: nil,
|
|
width: img.width, height: img.height,
|
|
bitsPerComponent: 8, bytesPerRow: img.width * 4,
|
|
space: CGColorSpaceCreateDeviceRGB(),
|
|
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
|
|
else { return nil }
|
|
|
|
context.interpolationQuality = .none
|
|
context.draw(img, in: CGRect(x: 0, y: 0, width: img.width, height: img.height))
|
|
|
|
return context
|
|
}
|
|
}
|