1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603 |
- /******************************************************************************
- Copyright (C) 2024 by Patrick Heyer <[email protected]>
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 2 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- ******************************************************************************/
- import Foundation
- import Metal
- private enum SampleVariant {
- case load
- case sample
- case sampleBias
- case sampleGrad
- case sampleLevel
- }
- private struct VariableType: OptionSet {
- var rawValue: UInt
- static let typeUniform = VariableType(rawValue: 1 << 0)
- static let typeStruct = VariableType(rawValue: 1 << 1)
- static let typeStructMember = VariableType(rawValue: 1 << 2)
- static let typeInput = VariableType(rawValue: 1 << 3)
- static let typeOutput = VariableType(rawValue: 1 << 4)
- static let typeTexture = VariableType(rawValue: 1 << 5)
- static let typeConstant = VariableType(rawValue: 1 << 6)
- }
- private struct OBSShaderFunction {
- let name: String
- var returnType: String
- var typeMap: [String: String]
- var requiresUniformBuffers: Bool
- var textures: [String]
- var samplers: [String]
- var arguments: [OBSShaderVariable]
- let gsFunction: UnsafeMutablePointer<shader_func>
- }
- private struct OBSShaderVariable {
- let name: String
- var type: String
- var mapping: String?
- var storageType: VariableType
- var requiredBy: Set<String>
- var returnedBy: Set<String>
- var isStage: Bool
- var attributeId: Int?
- var isConstant: Bool
- var isReference: Bool
- let gsVariable: UnsafeMutablePointer<shader_var>
- }
- private struct OBSShaderStruct {
- let name: String
- var storageType: VariableType
- var members: [OBSShaderVariable]
- let gsVariable: UnsafeMutablePointer<shader_struct>
- }
- private struct MSLTemplates {
- static let header = """
- #include <metal_stdlib>
- using namespace metal;
- """
- static let variable = "[qualifier] [type] [name] [mapping]"
- static let shaderStruct = """
- typedef struct {
- [variable]
- } [typename];
- """
- static let function = "[decorator] [type] [name]([parameters]) {[content]}"
- }
- private typealias ParserError = MetalError.OBSShaderParserError
- private typealias ShaderError = MetalError.OBSShaderError
- class OBSShader {
- private let type: MTLFunctionType
- private let content: String
- private let fileLocation: String
- private var parser: shader_parser
- private var parsed: Bool
- private var uniformsOrder = [String]()
- private var uniforms = [String: OBSShaderVariable]()
- private var structs = [String: OBSShaderStruct]()
- private var functionsOrder = [String]()
- private var functions = [String: OBSShaderFunction]()
- private var referenceVariables = [String]()
- var metaData: MetalShader.ShaderData?
- init(type: MTLFunctionType, content: String, fileLocation: String) throws {
- guard type == .vertex || type == .fragment else {
- throw ShaderError.unsupportedType
- }
- self.type = type
- self.content = content
- self.fileLocation = fileLocation
- self.parsed = false
- self.parser = shader_parser()
- try withUnsafeMutablePointer(to: &parser) {
- shader_parser_init($0)
- let result = shader_parse($0, content.cString(using: .utf8), content.cString(using: .utf8))
- let warnings = shader_parser_geterrors($0)
- if let warnings {
- throw ShaderError.parseError(String(cString: warnings))
- }
- if !result {
- throw ShaderError.parseFail("Shader failed to parse: \(fileLocation)")
- } else {
- self.parsed = true
- }
- }
- }
- /// Transpiles a `libobs` effect string into a Metal Shader Language (MSL) string
- /// - Returns: MSL string representing the transpiled shader
- func transpiled() throws -> String {
- try analyzeUniforms()
- try analyzeParameters()
- try analyzeFunctions()
- let uniforms = try transpileUniforms()
- let structs = try transpileStructs()
- let functions = try transpileFunctions()
- self.metaData = try buildMetadata()
- return [MSLTemplates.header, uniforms, structs, functions].joined(separator: "\n\n")
- }
- /// Builds a metadata object for the current shader
- /// - Returns: ``ShaderData`` object with the shader metadata
- ///
- /// The effects used by `libobs` are written in HLSL with some customizations to allow multiple shaders within the
- /// same effects file (which is supported natively by MSL). As MSL does not support "global" variables, uniforms
- /// have to be provided explicitly via buffers and the data inside those buffers needs to be laid out in the correct
- /// way.
- ///
- /// Uniforms are converted into `struct` objects in the shader files and as MSL is based on C++14, these structs
- /// will have a size, stride, and alignment, set by the compiler. Thus the uniform data used by the shader needs to
- /// be laid out in the buffer according to this alignment.
- ///
- /// The layout of vertex buffer data also needs to be communicated using `MTLVertexDescriptor` instances for vertex
- /// shaders and `MTLSamplerState` instances for fragment shaders. Both will be created and set up in a
- /// ``ShaderData`` which is used to create the actual ``MetalShader`` object.
- private func buildMetadata() throws -> MetalShader.ShaderData {
- var uniformInfo = [MetalShader.ShaderUniform]()
- var textureSlot = 0
- var uniformBufferSize = 0
- /// The order of buffers and uniforms is "load-bearing" as the order (and thus alignment and offsets) of
- /// uniforms in the corresponding uniforms struct are
- /// influenced by it.
- for uniformName in uniformsOrder {
- guard let uniform = uniforms[uniformName] else {
- throw ParserError.parseFail("No uniform data found for '\(uniformName)'")
- }
- let gsType = get_shader_param_type(uniform.gsVariable.pointee.type)
- let isTexture = uniform.storageType.contains(.typeTexture)
- let byteSize: Int
- let alignment: Int
- let bufferOffset: Int
- if isTexture {
- byteSize = 0
- alignment = 0
- bufferOffset = uniformBufferSize
- } else {
- byteSize = gsType.mtlSize
- alignment = gsType.mtlAlignment
- bufferOffset = (uniformBufferSize + (alignment - 1)) & ~(alignment - 1)
- }
- let shaderUniform = MetalShader.ShaderUniform(
- name: uniform.name,
- gsType: gsType,
- textureSlot: (isTexture ? textureSlot : 0),
- samplerState: nil,
- byteOffset: bufferOffset
- )
- shaderUniform.defaultValues = Array(
- UnsafeMutableBufferPointer(
- start: uniform.gsVariable.pointee.default_val.array,
- count: uniform.gsVariable.pointee.default_val.num)
- )
- shaderUniform.currentValues = shaderUniform.defaultValues
- uniformBufferSize = bufferOffset + byteSize
- if isTexture {
- textureSlot += 1
- }
- uniformInfo.append(shaderUniform)
- }
- guard let mainFunction = functions["main"] else {
- throw ParserError.missingMainFunction
- }
- let parameterMapper = { (mapping: String) -> MetalBuffer.BufferDataType? in
- switch mapping {
- case "POSITION":
- .vertex
- case "NORMAL":
- .normal
- case "TANGENT":
- .tangent
- case "COLOR":
- .color
- case _ where mapping.hasPrefix("TEXCOORD"):
- .texcoord
- default:
- .none
- }
- }
- let descriptorMapper = { (parameter: OBSShaderVariable) -> (MTLVertexFormat, Int)? in
- guard let mapping = parameter.mapping else {
- return nil
- }
- let type = parameter.type
- switch mapping {
- case "COLOR":
- return (.float4, MemoryLayout<vec4>.size)
- case "POSITION", "NORMAL", "TANGENT":
- return (.float4, MemoryLayout<vec4>.size)
- case _ where mapping.hasPrefix("TEXCOORD"):
- guard let numCoordinates = type[type.index(type.startIndex, offsetBy: 5)].wholeNumberValue else {
- assertionFailure("Unsupported type \(type) for texture parameter")
- return nil
- }
- let format: MTLVertexFormat =
- switch numCoordinates {
- case 0: .float
- case 2: .float2
- case 3: .float3
- case 4: .float4
- default: .invalid
- }
- guard format != .invalid else {
- assertionFailure("OBSShader: Unsupported amount of texture coordinates '\(numCoordinates)'")
- return nil
- }
- return (format, MemoryLayout<Float32>.size * numCoordinates)
- case "VERTEXID":
- return nil
- default:
- assertionFailure("OBSShader: Unsupported mapping \(mapping)")
- return nil
- }
- }
- switch type {
- case .vertex:
- var bufferOrder = [MetalBuffer.BufferDataType]()
- var descriptorData = [(MTLVertexFormat, Int)?]()
- let descriptor = MTLVertexDescriptor()
- for argument in mainFunction.arguments {
- if argument.storageType.contains(.typeStruct) {
- let actualStructType = argument.type.replacingOccurrences(of: "_In", with: "")
- guard let shaderStruct = structs[actualStructType] else {
- throw ParserError.parseFail("Shader function without struct metadata encountered ")
- }
- for shaderParameter in shaderStruct.members {
- if let mapping = shaderParameter.mapping, let mapping = parameterMapper(mapping) {
- bufferOrder.append(mapping)
- }
- if let description = descriptorMapper(shaderParameter) {
- descriptorData.append(description)
- }
- }
- } else {
- if let mapping = argument.mapping, let mapping = parameterMapper(mapping) {
- bufferOrder.append(mapping)
- }
- if let description = descriptorMapper(argument) {
- descriptorData.append(description)
- }
- }
- }
- let textureUnitCount = bufferOrder.filter({ $0 == .texcoord }).count
- for (attributeId, description) in descriptorData.filter({ $0 != nil }).enumerated() {
- descriptor.attributes[attributeId].bufferIndex = attributeId
- descriptor.attributes[attributeId].format = description!.0
- descriptor.layouts[attributeId].stride = description!.1
- }
- return MetalShader.ShaderData(
- uniforms: uniformInfo,
- bufferOrder: bufferOrder,
- vertexDescriptor: descriptor,
- samplerDescriptors: nil,
- bufferSize: uniformBufferSize,
- textureCount: textureUnitCount
- )
- case .fragment:
- var samplers = [MTLSamplerDescriptor]()
- for i in 0..<parser.samplers.num {
- let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i)
- if let sampler {
- var sampler_info = gs_sampler_info()
- shader_sampler_convert(sampler, &sampler_info)
- let borderColor: MTLSamplerBorderColor =
- switch sampler_info.border_color {
- case 0x00_00_00_FF:
- .opaqueBlack
- case 0xFF_FF_FF_FF:
- .opaqueWhite
- default:
- .transparentBlack
- }
- let descriptor = MTLSamplerDescriptor()
- descriptor.borderColor = borderColor
- descriptor.maxAnisotropy = Int(sampler_info.max_anisotropy)
- guard
- let sAddressMode = sampler_info.address_u.mtlMode,
- let tAddressMode = sampler_info.address_v.mtlMode,
- let rAddressMode = sampler_info.address_w.mtlMode,
- let minMagFilter = sampler_info.filter.minMagFilter,
- let mipFilter = sampler_info.filter.mipFilter
- else {
- samplers.append(descriptor)
- continue
- }
- descriptor.sAddressMode = sAddressMode
- descriptor.tAddressMode = tAddressMode
- descriptor.rAddressMode = rAddressMode
- descriptor.minFilter = minMagFilter
- descriptor.magFilter = minMagFilter
- descriptor.mipFilter = mipFilter
- samplers.append(descriptor)
- }
- }
- return MetalShader.ShaderData(
- uniforms: uniformInfo,
- bufferOrder: [],
- vertexDescriptor: nil,
- samplerDescriptors: samplers,
- bufferSize: uniformBufferSize,
- textureCount: 0
- )
- default:
- throw ShaderError.unsupportedType
- }
- }
- /// Analyzes shader uniform parameters parsed by the ``libobs`` shader parser.
- ///
- /// Each global variable declared as a "uniform" is stored as an ``OBSShaderVariable`` struct, which will be
- /// extended with additional metadata by later analystics steps.
- ///
- /// This is necessary as MSL does not support global variables and all data needs to be explicitly provided
- /// via buffer objects, which requires these "unforms" to be wrapped into a single struct and passed as an explicit
- /// buffer object.
- private func analyzeUniforms() throws {
- for i in 0..<parser.params.num {
- let uniform: UnsafeMutablePointer<shader_var>? = parser.params.array.advanced(by: i)
- guard let uniform, let name = uniform.pointee.name, let type = uniform.pointee.type else {
- throw ParserError.parseFail("Uniform is missing name or type information")
- }
- let mapping: String? =
- if let mapping = uniform.pointee.mapping {
- String(cString: mapping)
- } else {
- nil
- }
- var data = OBSShaderVariable(
- name: String(cString: name),
- type: String(cString: type),
- mapping: mapping,
- storageType: .typeUniform,
- requiredBy: [],
- returnedBy: [],
- isStage: false,
- attributeId: 0,
- isConstant: (uniform.pointee.var_type == SHADER_VAR_CONST),
- isReference: false,
- gsVariable: uniform
- )
- if self.type == .fragment {
- /// A texture uniform does not contribute to the uniform buffer
- if data.type.hasPrefix("texture") {
- data.storageType.remove(.typeUniform)
- data.storageType.insert(.typeTexture)
- }
- }
- uniformsOrder.append(data.name)
- uniforms.updateValue(data, forKey: data.name)
- }
- }
- /// Analyzes struct parameter declarations parsed by the ``libobs`` shader parser.
- ///
- /// Structured data declarations are used to pass data into and out of shaders.
- ///
- /// Whereas HLSL allows one to use "InOut" structures with attribute mappings (e.g., using the same type defintion
- /// for vertex data going in and out of a vertex shader), MSL does not allow the mixing of input mappings and output
- /// mappings in the same type definition.
- ///
- /// Thus when the same struct type is used as an input argument for a function but also used as its output type, it
- /// needs to be split up into two separate types for the MSL shader.
- ///
- /// This function will first detect all struct type definitions in the shader file and then check if it is used as
- /// an input argument or function output and update the associated ``OBSShaderVariable`` structs accordingly.
- private func analyzeParameters() throws {
- for i in 0..<parser.structs.num {
- let shaderStruct: UnsafeMutablePointer<shader_struct>? = parser.structs.array.advanced(by: i)
- guard let shaderStruct, let name = shaderStruct.pointee.name else {
- throw ParserError.parseFail("Constant data struct has no name")
- }
- var parameters = [OBSShaderVariable]()
- parameters.reserveCapacity(shaderStruct.pointee.vars.num)
- for j in 0..<shaderStruct.pointee.vars.num {
- let variablePointer: UnsafeMutablePointer<shader_var>? = shaderStruct.pointee.vars.array.advanced(by: j)
- guard let variablePointer, let variableName = variablePointer.pointee.name,
- let variableType = variablePointer.pointee.type
- else {
- throw ParserError.parseFail("Constant data variable has no name")
- }
- let mapping: String? =
- if let variableMapping = variablePointer.pointee.mapping { String(cString: variableMapping) } else {
- nil
- }
- let variable = OBSShaderVariable(
- name: String(cString: variableName),
- type: String(cString: variableType),
- mapping: mapping,
- storageType: .typeStructMember,
- requiredBy: [],
- returnedBy: [],
- isStage: false,
- attributeId: nil,
- isConstant: false,
- isReference: false,
- gsVariable: variablePointer
- )
- parameters.append(variable)
- }
- let data = OBSShaderStruct(
- name: String(cString: name),
- storageType: [],
- members: parameters,
- gsVariable: shaderStruct
- )
- structs.updateValue(data, forKey: data.name)
- }
- for i in 0..<parser.funcs.num {
- let function: UnsafeMutablePointer<shader_func>? = parser.funcs.array.advanced(by: i)
- guard let function, let functionName = function.pointee.name, let returnType = function.pointee.return_type
- else {
- throw ParserError.parseFail("Shader function has no name or type information")
- }
- var functionData = OBSShaderFunction(
- name: String(cString: functionName),
- returnType: String(cString: returnType),
- typeMap: [:],
- requiresUniformBuffers: false,
- textures: [],
- samplers: [],
- arguments: [],
- gsFunction: function,
- )
- for j in 0..<function.pointee.params.num {
- let parameter: UnsafeMutablePointer<shader_var>? = function.pointee.params.array.advanced(by: j)
- guard let parameter, let parameterName = parameter.pointee.name,
- let parameterType = parameter.pointee.type
- else {
- throw ParserError.parseFail("Function parameter has no name or type information")
- }
- let mapping: String? =
- if let parameterMapping = parameter.pointee.mapping {
- String(cString: parameterMapping)
- } else {
- nil
- }
- /// Most effects do not seem to use `out` or `inout` function arguments, but the lanczos scale filter
- /// does. The most straight-forward way
- /// to support this pattern is to use C++-style references with the `thread` storage specifier.
- let isReferenceVariable =
- (parameter.pointee.var_type == SHADER_VAR_OUT || parameter.pointee.var_type == SHADER_VAR_INOUT)
- var parameterData = OBSShaderVariable(
- name: String(cString: parameterName),
- type: String(cString: parameterType),
- mapping: mapping,
- storageType: .typeInput,
- requiredBy: [functionData.name],
- returnedBy: [],
- isStage: false,
- attributeId: nil,
- isConstant: (parameter.pointee.var_type == SHADER_VAR_CONST),
- isReference: isReferenceVariable,
- gsVariable: parameter
- )
- if isReferenceVariable {
- referenceVariables.append(parameterData.name)
- }
- if parameterData.type == functionData.returnType {
- parameterData.returnedBy.insert(functionData.name)
- }
- if !functionData.typeMap.keys.contains(parameterData.name) {
- functionData.typeMap.updateValue(parameterData.type, forKey: parameterData.name)
- }
- /// Metal does not support using the same attribute mappings for structs as input to shader functions
- /// and output. They need to use different
- /// mappings and thus every "InOut" struct by `libobs` needs to be split up into a separate input and
- /// output struct type.
- for var shaderStruct in structs.values {
- if shaderStruct.name == parameterData.type {
- shaderStruct.storageType.insert(.typeInput)
- parameterData.storageType.insert(.typeStruct)
- if shaderStruct.name == functionData.returnType {
- shaderStruct.storageType.insert(.typeOutput)
- parameterData.storageType.insert(.typeOutput)
- parameterData.type.append("_In")
- functionData.returnType.append("_Out")
- }
- structs.updateValue(shaderStruct, forKey: shaderStruct.name)
- }
- }
- functionData.arguments.append(parameterData)
- }
- if var shaderStruct = structs[functionData.returnType] {
- shaderStruct.storageType.insert(.typeOutput)
- structs.updateValue(shaderStruct, forKey: shaderStruct.name)
- }
- functions.updateValue(functionData, forKey: functionData.name)
- }
- }
- /// Analyzes function data parsed by the ``libobs`` shader parser
- ///
- /// As MSL does not support uniforms or using the same struct type for input and output, function bodies themselves
- /// need to be parsed again and checked for their usage of these types or variables.
- ///
- /// Due to the way that the ``libobs`` parser works, each body of a block (either within curly braces or
- /// parentheses) is analyzed recursively and updating the same ``OBSShaderFunction`` struct.
- ///
- /// After a full analysis pass, this struct should contain information about all uniforms, textures, and samplers
- /// used (or passed on) by the function.
- private func analyzeFunctions() throws {
- for i in 0..<parser.funcs.num {
- let function: UnsafeMutablePointer<shader_func>? = parser.funcs.array.advanced(by: i)
- guard var function, var token = function.pointee.start, let functionName = function.pointee.name else {
- throw ParserError.parseFail("Shader function has no name")
- }
- let functionData = functions[String(cString: functionName)]
- guard var functionData else {
- throw ParserError.parseFail("Shader function without function meta data encountered")
- }
- try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: "}")
- functionData.textures = functionData.textures.unique()
- functionData.samplers = functionData.samplers.unique()
- functions.updateValue(functionData, forKey: functionData.name)
- functionsOrder.append(functionData.name)
- }
- }
- /// Analyzes a function body or source scope to check for use of global variables, textures, or samplers.
- ///
- /// Because MSL does not support global variables, unforms, textures, or samplers need to be passed explicitly to a
- /// function. This requires scanning the entire function body (recursively in the case of separate function scopes
- /// denoted by curvy brackets or parantheses) for any occurrence of a known uniform, texture, or sampler variable
- /// name.
- ///
- /// - Parameters:
- /// - function: Pointer to a ``shader_func`` element representing a parsed shader function
- /// - functionData: Reference to a ``OBSShaderFunction`` struct, which will be updated by this function
- /// - token: Pointer to a ``cf_token`` element used to interact with the shader parser provided by ``libobs``
- /// - end: The sentinel character at which analysis (and parsing) should stop
- private func analyzeFunction(
- function: inout UnsafeMutablePointer<shader_func>, functionData: inout OBSShaderFunction,
- token: inout UnsafeMutablePointer<cf_token>, end: String
- ) throws {
- let uniformNames =
- (uniforms.filter {
- !$0.value.storageType.contains(.typeTexture)
- }).keys
- while token.pointee.type != CFTOKEN_NONE {
- token = token.successor()
- if token.pointee.str.isEqualTo(end) {
- break
- }
- let stringToken = token.pointee.str.getString()
- if token.pointee.type == CFTOKEN_NAME {
- if uniformNames.contains(stringToken) && functionData.requiresUniformBuffers == false {
- functionData.requiresUniformBuffers = true
- }
- if let function = functions[stringToken] {
- if function.requiresUniformBuffers && functionData.requiresUniformBuffers == false {
- functionData.requiresUniformBuffers = true
- }
- functionData.textures.append(contentsOf: function.textures)
- functionData.samplers.append(contentsOf: function.samplers)
- }
- if type == .fragment {
- for uniform in uniforms.values {
- if stringToken == uniform.name && uniform.storageType.contains(.typeTexture) {
- functionData.textures.append(stringToken)
- }
- }
- for i in 0..<parser.samplers.num {
- let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i)
- guard let sampler, let samplerName = sampler.pointee.name else {
- break
- }
- if stringToken == String(cString: samplerName) {
- functionData.samplers.append(stringToken)
- }
- }
- }
- } else if token.pointee.type == CFTOKEN_OTHER {
- if token.pointee.str.isEqualTo("{") {
- try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: "}")
- } else if token.pointee.str.isEqualTo("(") {
- try analyzeFunction(function: &function, functionData: &functionData, token: &token, end: ")")
- }
- }
- }
- }
- /// Transpiles the uniform global variables used by the shader into a `UniformData` struct that contains the
- /// uniforms.
- /// - Returns: String representing the uniform data struct
- private func transpileUniforms() throws -> String {
- var output = [String]()
- for uniformName in uniformsOrder {
- if var uniform = uniforms[uniformName] {
- uniform.isStage = false
- uniform.attributeId = 0
- if !uniform.storageType.contains(.typeTexture) {
- let variableString = try transpileVariable(variable: uniform)
- output.append("\(variableString);")
- }
- }
- }
- if output.count > 0 {
- let replacements = [
- ("[variable]", output.joined(separator: "\n")),
- ("[typename]", "UniformData"),
- ]
- let uniformString = replacements.reduce(into: MSLTemplates.shaderStruct) { string, replacement in
- string = string.replacingOccurrences(of: replacement.0, with: replacement.1)
- }
- return uniformString
- } else {
- return ""
- }
- }
- /// Transpiles the vertex data structs used by the shader
- /// - Returns: String representing the vertex data structs
- private func transpileStructs() throws -> String {
- var output = [String]()
- for var shaderStruct in structs.values {
- if shaderStruct.storageType.isSuperset(of: [.typeInput, .typeOutput]) {
- /// Metal does not support using the same attribute mappings for structs as input to shader functions
- /// and output. They need to use different mappings and thus every "InOut" struct by `libobs` needs to
- /// be split up into a separate input and output struct type.
- for suffix in ["_In", "_Out"] {
- var variables = [String]()
- for (structVariableId, var structVariable) in shaderStruct.members.enumerated() {
- let variableString: String
- switch suffix {
- case "_In":
- structVariable.storageType.formUnion([.typeInput])
- structVariable.attributeId = structVariableId
- variableString = try transpileVariable(variable: structVariable)
- structVariable.storageType.remove([.typeInput])
- case "_Out":
- structVariable.storageType.formUnion([.typeOutput])
- variableString = try transpileVariable(variable: structVariable)
- structVariable.storageType.remove([.typeOutput])
- default:
- throw ParserError.parseFail("Shader struct with unknown prefix encountered")
- }
- variables.append("\(variableString);")
- shaderStruct.members[structVariableId] = structVariable
- }
- let replacements = [
- ("[variable]", variables.joined(separator: "\n")),
- ("[typename]", "\(shaderStruct.name)\(suffix)"),
- ]
- let result = replacements.reduce(into: MSLTemplates.shaderStruct) {
- string, replacement in
- string = string.replacingOccurrences(of: replacement.0, with: replacement.1)
- }
- output.append(result)
- }
- } else {
- var variables = [String]()
- for (structVariableId, var structVariable) in shaderStruct.members.enumerated() {
- if shaderStruct.storageType.contains(.typeInput) {
- structVariable.storageType.insert(.typeInput)
- structVariable.attributeId = structVariableId
- } else if shaderStruct.storageType.contains(.typeOutput) {
- structVariable.storageType.insert(.typeOutput)
- }
- let variableString = try transpileVariable(variable: structVariable)
- structVariable.storageType.subtract([.typeInput, .typeOutput])
- variables.append("\(variableString);")
- shaderStruct.members[structVariableId] = structVariable
- }
- let replacements = [
- ("[variable]", variables.joined(separator: "\n")),
- ("[typename]", shaderStruct.name),
- ]
- let result = replacements.reduce(into: MSLTemplates.shaderStruct) {
- string, replacement in
- string = string.replacingOccurrences(of: replacement.0, with: replacement.1)
- }
- output.append(result)
- }
- }
- if output.count > 0 {
- return output.joined(separator: "\n\n")
- } else {
- return ""
- }
- }
- /// Transpiles a shader function into its MSL variant
- /// - Returns: String representing the transpiled MSL shader function
- private func transpileFunctions() throws -> String {
- var output = [String]()
- for functionName in functionsOrder {
- guard let function = functions[functionName], var token = function.gsFunction.pointee.start else {
- throw ParserError.parseFail("Shader function has no name")
- }
- var stageConsumed = false
- let isMain = functionName == "main"
- var variables = [String]()
- for var variable in function.arguments {
- if isMain && !stageConsumed {
- variable.isStage = true
- stageConsumed = true
- }
- try variables.append(transpileVariable(variable: variable))
- }
- /// As Metal has no support for global constants, the constant data needs to be wrapped into a `struct`
- /// and the associated data is uploaded into a vertex buffer at a specific index (30 in this case).
- ///
- /// Buffers are not automatically available to shader functions but are passed into the function explicitly
- ///as arguments.
- ///
- /// As `libobs` effects are based around a "main" entry function (something strongly discouraged by Metal),
- /// each "main" function needs to receive the actual buffer as an argument and each function called _by_
- /// the main function and which internally accesses the uniform needs to have that uniform passed
- /// explicitly as an argument as well.
- if (uniforms.values.filter { !$0.storageType.contains(.typeTexture) }).count > 0 {
- if isMain {
- variables.append("constant UniformData &uniforms [[buffer(30)]]")
- } else if function.requiresUniformBuffers {
- variables.append("constant UniformData &uniforms")
- }
- }
- if type == .fragment {
- var textureId = 0
- for uniformName in uniformsOrder {
- guard let uniform = uniforms[uniformName] else {
- break
- }
- if uniform.storageType.contains(.typeTexture) {
- if isMain {
- let variableString = try transpileVariable(variable: uniform)
- variables.append("\(variableString) [[texture(\(textureId))]]")
- textureId += 1
- } else if function.textures.contains(uniform.name) {
- let variableString = try transpileVariable(variable: uniform)
- variables.append(variableString)
- }
- }
- }
- var samplerId = 0
- for i in 0..<parser.samplers.num {
- let sampler: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i)
- if let sampler, let samplerName = sampler.pointee.name {
- let name = String(cString: samplerName)
- if isMain {
- let variableString = "sampler \(name) [[sampler(\(samplerId))]]"
- variables.append(variableString)
- samplerId += 1
- } else if function.samplers.contains(name) {
- let variabelString = "sampler \(name)"
- variables.append(variabelString)
- }
- }
- }
- }
- let mappedType = try convertToMTLType(gsType: function.returnType)
- let functionContent: String
- var replacements = [(String, String)]()
- /// Metal shaders do not have "main" functions - a single shader file usually contains all shader functions
- /// used by an application, each identified by their name and type decorator. This is not supported by OBS,
- /// so each shader needs to have a "main" function that calls the actual shader function, which thus
- /// requires a new shader library to be created for each effect file.
- if isMain {
- replacements = [
- ("[name]", "_main"),
- ("[parameters]", variables.joined(separator: ", ")),
- ]
- switch type {
- case .vertex:
- replacements.append(("[decorator]", "[[vertex]]"))
- case .fragment:
- replacements.append(("[decorator]", "[[fragment]]"))
- default:
- fatalError("OBSShader: Unsupported shader type \(type)")
- }
- let temporaryContent = try transpileFunctionContent(token: &token, end: "}")
- if type == .fragment && isMain && mappedType == "float3" {
- replacements.append(("[type]", "float4"))
- // TODO: Replace with Swift-native Regex once macOS 13+ is minimum target
- let regex = try NSRegularExpression(pattern: "return (.+);")
- functionContent = regex.stringByReplacingMatches(
- in: temporaryContent,
- range: NSRange(location: 0, length: temporaryContent.count),
- withTemplate: "return float4($1, 1);"
- )
- } else {
- functionContent = temporaryContent
- replacements.append(("[type]", mappedType))
- }
- replacements.append(("[content]", functionContent))
- } else {
- functionContent = try transpileFunctionContent(token: &token, end: "}")
- replacements = [
- ("[decorator]", ""),
- ("[type]", mappedType),
- ("[name]", function.name),
- ("[parameters]", variables.joined(separator: ", ")),
- ("[content]", functionContent),
- ]
- }
- let result = replacements.reduce(into: MSLTemplates.function) {
- string, replacement in
- string = string.replacingOccurrences(of: replacement.0, with: replacement.1)
- }
- output.append(result)
- }
- if output.count > 0 {
- return output.joined(separator: "\n\n")
- } else {
- return ""
- }
- }
- /// Transpiles a variable into its MSL variant
- /// - Parameter variable: Variable to transpile
- /// - Returns: String representing a transpiled variable
- ///
- /// Variables can either be members of a `struct` or an argument to a function. The ``OBSShaderVariable`` instance
- /// has a `storageType` property which encodes the use of the variable and helps in creation of the appropriate MSL
- /// string representation.
- private func transpileVariable(variable: OBSShaderVariable) throws -> String {
- var mappings = [String]()
- var metalMapping: String
- var indent = 0
- let metalType = try convertToMTLType(gsType: variable.type)
- if variable.storageType.contains(.typeUniform) {
- indent = 4
- } else if variable.storageType.isSuperset(of: [.typeInput, .typeStructMember]) {
- switch type {
- case .vertex:
- indent = 4
- /// Attributes are used to associate a member of a uniform `struct` with its data in the vertex buffer
- /// stage.
- if let attributeId = variable.attributeId {
- mappings.append("attribute(\(attributeId))")
- }
- case .fragment:
- indent = 4
- if let mappingPointer = variable.gsVariable.pointee.mapping,
- let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer))
- {
- mappings.append(mappedString)
- }
- default:
- fatalError("OBSShader: Unsupported shader function type \(type)")
- }
- } else if variable.storageType.isSuperset(of: [.typeOutput, .typeStructMember]) {
- indent = 4
- if let mappingPointer = variable.gsVariable.pointee.mapping,
- let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer))
- {
- mappings.append(mappedString)
- }
- } else {
- indent = 0
- if variable.isStage {
- if let mappingPointer = variable.gsVariable.pointee.mapping,
- let mappedString = convertToMTLMapping(gsMapping: String(cString: mappingPointer))
- {
- mappings.append(mappedString)
- } else {
- mappings.append("stage_in")
- }
- }
- }
- if mappings.count > 0 {
- metalMapping = " [[\(mappings.joined(separator: ", "))]]"
- } else {
- metalMapping = ""
- }
- let qualifier =
- if variable.storageType.contains(.typeConstant) {
- " constant "
- } else if variable.isReference {
- " thread "
- } else {
- ""
- }
- let name =
- if variable.isReference {
- "&\(variable.name)"
- } else { variable.name }
- let result = "\(String(repeating: " ", count: indent))\(qualifier)\(metalType) \(name)\(metalMapping)"
- return result
- }
- /// Transpiles the body of a function into its MSL representation
- /// - Parameters:
- /// - token: Stateful `libobs` parser token pointer
- /// - end: String representing which ends function body parsing if matched
- /// - Returns: String representing the body of a MSL shader function
- ///
- /// OBS effect function content needs to be transpiled into MSL function content token by token, as each token
- /// needs to be matched not only against direct translations (e.g., a HLSL function name into its appropriate MSL
- /// variant) but also to detect if a token represents a uniform variable which will not be available as a global
- /// variable in MSL, but instead will only exist as part of the `uniform` struct that was explicitly passed into
- /// the function.
- ///
- /// Similarly, if a function call is encountered, the function's metadata needs to be checked for use of such a
- /// uniform and the call signature extended to explicitly pass the data into the called function.
- ///
- /// Because Metal does not implicitly or automagically coerce types (but the effects files sometimes rely on this),
- /// some arguments and parameters need to be explicitly wrapped in casts to wider types (e.g., a `float3` is
- /// returned from a fragment shader, but fragment shaders _have to_ provide a `float4`).
- ///
- /// There are many such conversions necessary, as MSL is more strict than HLSL or GLSL when it comes to type safety.
- private func transpileFunctionContent(token: inout UnsafeMutablePointer<cf_token>, end: String) throws -> String {
- var content = [String]()
- while token.pointee.type != CFTOKEN_NONE {
- token = token.successor()
- if token.pointee.str.isEqualTo(end) {
- break
- }
- let stringToken = token.pointee.str.getString()
- if token.pointee.type == CFTOKEN_NAME {
- let type = try convertToMTLType(gsType: stringToken)
- if stringToken == "obs_glsl_compile" {
- content.append("false")
- continue
- }
- if type != stringToken {
- content.append(type)
- continue
- }
- if let intrinsic = try convertToMTLIntrinsic(intrinsic: stringToken) {
- content.append(intrinsic)
- continue
- }
- if stringToken == "mul" {
- try content.append(convertToMTLMultiplication(token: &token))
- continue
- } else if stringToken == "mad" {
- try content.append(convertToMTLMultiplyAdd(token: &token))
- continue
- } else {
- var skip = false
- for uniform in uniforms.values {
- if uniform.name == stringToken && uniform.storageType.contains(.typeTexture) {
- try content.append(createSampler(token: &token))
- skip = true
- break
- }
- }
- if skip {
- continue
- }
- }
- if uniforms.keys.contains(stringToken) {
- let priorToken = token.predecessor()
- let priorString = priorToken.pointee.str.getString()
- if priorString != "." {
- content.append("uniforms.\(stringToken)")
- continue
- }
- }
- var skip = false
- for shaderStruct in structs.values {
- if shaderStruct.name == stringToken {
- if shaderStruct.storageType.isSuperset(of: [.typeInput, .typeOutput]) {
- content.append("\(stringToken)_Out")
- skip = true
- break
- }
- }
- }
- if skip {
- continue
- }
- if let comparison = try convertToMTLComparison(token: &token) {
- content.append(comparison)
- continue
- }
- content.append(stringToken)
- } else if token.pointee.type == CFTOKEN_OTHER {
- if token.pointee.str.isEqualTo("{") {
- let blockContent = try transpileFunctionContent(token: &token, end: "}")
- content.append("{\(blockContent)}")
- continue
- } else if token.pointee.str.isEqualTo("(") {
- let priorToken = token.predecessor()
- let functionName = priorToken.pointee.str.getString()
- var functionParameters = [String]()
- let parameters = try transpileFunctionContent(token: &token, end: ")")
- if functionName == "int3" {
- let intParameters = parameters.split(
- separator: ",", maxSplits: 3, omittingEmptySubsequences: true)
- switch intParameters.count {
- case 3:
- functionParameters.append(
- "int(\(intParameters[0])), int(\(intParameters[1])), int(\(intParameters[2]))")
- case 2:
- functionParameters.append("int2(\(intParameters[0])), int(\(intParameters[1]))")
- case 1:
- functionParameters.append("\(intParameters)")
- default:
- throw ParserError.parseFail("int3 constructor with invalid amount of arguments encountered")
- }
- } else {
- functionParameters.append(parameters)
- }
- if let additionalArguments = generateAdditionalArguments(for: functionName) {
- functionParameters.append(additionalArguments)
- }
- content.append("(\(functionParameters.joined(separator: ", ")))")
- continue
- }
- content.append(stringToken)
- } else {
- content.append(stringToken)
- }
- }
- return content.joined()
- }
- /// Converts a HLSL-like type into a MSL type if possible
- /// - Parameter gsType: HLSL-like type string
- /// - Returns: MSL type string
- private func convertToMTLType(gsType: String) throws -> String {
- switch gsType {
- case "texture2d":
- return "texture2d<float>"
- case "texture3d":
- return "texture3d<float>"
- case "texture_cube":
- return "texturecube<float>"
- case "texture_rect":
- throw ParserError.unsupportedType
- case "half2":
- return "float2"
- case "half3":
- return "float3"
- case "half4":
- return "float4"
- case "half":
- return "float"
- case "min16float2":
- return "half2"
- case "min16float3":
- return "half3"
- case "min16float4":
- return "half4"
- case "min16float":
- return "half"
- case "min10float":
- throw ParserError.unsupportedType
- case "double":
- throw ParserError.unsupportedType
- case "min16int2":
- return "short2"
- case "min16int3":
- return "short3"
- case "min16int4":
- return "short4"
- case "min16int":
- return "short"
- case "min16uint2":
- return "ushort2"
- case "min16uint3":
- return "ushort3"
- case "min16uint4":
- return "ushort4"
- case "min16uint":
- return "ushort"
- case "min13int":
- throw ParserError.unsupportedType
- default:
- return gsType
- }
- }
- /// Converts an HLSL-like uniform mapping into a MSL attribute decoration if possible
- /// - Parameter gsMapping: HLSL-like mapping
- /// - Returns: MSL attribute string
- private func convertToMTLMapping(gsMapping: String) -> String? {
- switch gsMapping {
- case "POSITION":
- return "position"
- case "VERTEXID":
- return "vertex_id"
- default:
- return nil
- }
- }
- /// Converts a HLSL-like comparison to a vector-safe MSL comparison operation
- /// - Parameter token: Start token of the comparison in the function body
- /// - Returns: MSL comparison operation
- ///
- /// A comparison operation that involves a vector will always result in a boolean vector in MSL (and not a scalar
- /// vector). Thus any functions that compares two vectors will also result in a vector
- /// (e.g., float2 == float2 -> bool2). This will break when a ternary expression is used, as the first element of
- /// it needs to be as scalar boolean in MSL.
- ///
- /// Wrapping the comparison in `all` ensures that a single scalar `true` is returned if all elements of the
- /// resulting boolean vectors are `true` as well.
- private func convertToMTLComparison(token: inout UnsafeMutablePointer<cf_token>) throws -> String? {
- var isComparator = false
- let nextToken = token.successor()
- if nextToken.pointee.type == CFTOKEN_OTHER {
- let comparators = ["==", "!=", "<", "<=", ">=", ">"]
- for comparator in comparators {
- if nextToken.pointee.str.isEqualTo(comparator) {
- isComparator = true
- break
- }
- }
- }
- if isComparator {
- var cfp = parser.cfp
- cfp.cur_token = token
- let lhs = cfp.cur_token.pointee.str.getString()
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- let comparator = cfp.cur_token.pointee.str.getString()
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- let rhs = cfp.cur_token.pointee.str.getString()
- return "all(\(lhs) \(comparator) \(rhs))"
- } else {
- return nil
- }
- }
- /// Converts HLSL-like intrinsic into its MSL representation
- /// - Parameter intrinsic: HLSL-like intrinsic string
- /// - Returns: MSL intrinsic string
- private func convertToMTLIntrinsic(intrinsic: String) throws -> String? {
- switch intrinsic {
- case "clip":
- throw ParserError.unsupportedType
- case "ddx":
- return "dfdx"
- case "ddy":
- return "dfdy"
- case "frac":
- return "fract"
- case "lerp":
- return "mix"
- default:
- return nil
- }
- }
- /// Converts a HLSL-like multiplication function call into a direct multiplication
- /// - Parameter token: Start token of the multiplication in the function body
- /// - Returns: MSL multiplication string
- private func convertToMTLMultiplication(token: inout UnsafeMutablePointer<cf_token>) throws -> String {
- var cfp = parser.cfp
- cfp.cur_token = token
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken }
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let lhs = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- cfp.cur_token = cfp.cur_token.predecessor()
- let rhs = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "(\(lhs)) * (\(rhs))"
- }
- /// Converts a HLSL-like multiply+add function call into a direct multiplication followed by addition
- /// - Parameter token: Start token of the multiply+add in the function body
- /// - Returns: MSL multiplication and addition string
- private func convertToMTLMultiplyAdd(token: inout UnsafeMutablePointer<cf_token>) throws -> String {
- var cfp = parser.cfp
- cfp.cur_token = token
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken }
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "((\(first)) * (\(second))) + (\(third))"
- }
- /// Creates an MSL sampler call from a HLSL-like sampler call
- /// - Parameter token: Start token of the sampler call in the function
- /// - Returns: String of an MSL sampler call
- private func createSampler(token: inout UnsafeMutablePointer<cf_token>) throws -> String {
- var cfp = parser.cfp
- cfp.cur_token = token
- let stringToken = token.pointee.str.getString()
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- guard cfp.tokenIsEqualTo(".") else { throw ParserError.unexpectedToken }
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- guard cfp.cur_token.pointee.type == CFTOKEN_NAME else { throw ParserError.unexpectedToken }
- let textureCall: String
- if cfp.tokenIsEqualTo("Sample") {
- textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sample)
- } else if cfp.tokenIsEqualTo("SampleBias") {
- textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleBias)
- } else if cfp.tokenIsEqualTo("SampleGrad") {
- textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleGrad)
- } else if cfp.tokenIsEqualTo("SampleLevel") {
- textureCall = try createTextureCall(token: &cfp.cur_token, callType: .sampleLevel)
- } else if cfp.tokenIsEqualTo("Load") {
- textureCall = try createTextureCall(token: &cfp.cur_token, callType: .load)
- } else {
- throw ParserError.missingNextToken
- }
- token = cfp.cur_token
- return "\(stringToken).\(textureCall)"
- }
- /// Creates a MSL sampler call based on the sampling type
- /// - Parameters:
- /// - token: Start token of the sampler call arguments in the function body
- /// - callType: Type of sampling used
- /// - Returns: String of an MSL sampler call
- private func createTextureCall(token: inout UnsafeMutablePointer<cf_token>, callType: SampleVariant) throws
- -> String
- {
- var cfp = parser.cfp
- cfp.cur_token = token
- guard cfp.advanceToken() else { throw ParserError.missingNextToken }
- guard cfp.tokenIsEqualTo("(") else { throw ParserError.unexpectedToken }
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- switch callType {
- case .sample:
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let second = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "sample(\(first), \(second))"
- case .sampleBias:
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "sample(\(first), \(second), bias(\(third)))"
- case .sampleGrad:
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let third = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let fourth = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "sample(\(first), \(second), gradient2d(\(third), \(fourth)))"
- case .sampleLevel:
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let second = try transpileFunctionContent(token: &cfp.cur_token, end: ",")
- guard cfp.hasNextToken() else { throw ParserError.missingNextToken }
- let third = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- token = cfp.cur_token
- return "sample(\(first), \(second), level(\(third)))"
- case .load:
- let first = try transpileFunctionContent(token: &cfp.cur_token, end: ")")
- let loadCall: String
- /// Many load calls in OBS effects files rely on implicit type conversion, which is not allowed in MSL in
- /// addition to `read` calls only accepting a `uint2` followed by a `uint`. Any instance of a `int3` thus
- /// needs to be converted into the appropriate variant compatible with the `read` call.
- if first.hasPrefix("int3(") {
- let loadParameters = first[
- first.index(first.startIndex, offsetBy: 5)..<first.index(first.endIndex, offsetBy: -1)
- ].split(separator: ",", maxSplits: 3, omittingEmptySubsequences: true)
- switch loadParameters.count {
- case 3:
- loadCall = "read(uint2(\(loadParameters[0]), \(loadParameters[1])), uint(\(loadParameters[2])))"
- case 2:
- loadCall = "read(uint2(\(loadParameters[0])), uint(\(loadParameters[1])))"
- case 1:
- loadCall = "read(uint2(\(loadParameters[0]).xy), 0)"
- default:
- throw ParserError.parseFail("int3 constructor with invalid number of arguments encountered")
- }
- } else {
- loadCall = "read(uint2(\(first).xy), 0)"
- }
- token = cfp.cur_token
- return loadCall
- }
- }
- /// Generates the explicit arguments that need to be passed into MSL shader functions in place of direct access to
- /// uniform globals which are not supported by Metal.
- /// - Parameter functionName: Name of the function to generate the additional arguments for
- /// - Returns: String of additional arguments to be put into the function signature
- private func generateAdditionalArguments(for functionName: String) -> String? {
- var output = [String]()
- for function in functions.values {
- if function.name != functionName {
- continue
- }
- if function.requiresUniformBuffers {
- output.append("uniforms")
- }
- for texture in function.textures {
- for uniform in uniforms.values {
- if uniform.name == texture && uniform.storageType.contains(.typeTexture) {
- output.append(texture)
- }
- }
- }
- for sampler in function.samplers {
- for i in 0..<parser.samplers.num {
- let samplerPointer: UnsafeMutablePointer<shader_sampler>? = parser.samplers.array.advanced(by: i)
- if let samplerPointer {
- if sampler == String(cString: samplerPointer.pointee.name) {
- output.append(sampler)
- }
- }
- }
- }
- }
- if output.count > 0 {
- return output.joined(separator: ", ")
- }
- return nil
- }
- deinit {
- withUnsafeMutablePointer(to: &parser) {
- shader_parser_free($0)
- }
- }
- }
|