/****************************************************************************** Copyright (C) 2024 by Patrick Heyer 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 . ******************************************************************************/ import Foundation import Metal private typealias ParserError = MetalError.OBSShaderParserError private typealias ShaderError = MetalError.OBSShaderError private typealias MetalShaderError = MetalError.MetalShaderError /// Creates a ``MetalShader`` instance from the given shader string for use as a vertex shader. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - shader: C character pointer with the contents of the `libobs` effect file /// - file: C character pointer with the contents of the `libobs` effect file location /// - error_string: Pointer for another C character pointer with the contents of an error description /// - Returns: Opaque pointer to a new ``MetalShader`` instance on success or `nil` on error /// /// The string pointed to by the `data` argument is a re-compiled shader string created from the associated "effect" /// file (which will contain multiple effects). Each effect is made up of several passes (though usually only a single /// pass is defined), each of which contains a vertex and fragment shader. This function is then called with just the /// vertex shader string. /// /// This vertex shader string needs to be parsed again and transpiled into a Metal shader string, which is handled by /// the ``OBSShader`` class. The transpiled string is then used to create the actual ``MetalShader`` instance. @_cdecl("device_vertexshader_create") public func device_vertexshader_create( device: UnsafeRawPointer, shader: UnsafePointer, file: UnsafePointer, error_string: UnsafeMutablePointer> ) -> OpaquePointer? { let device: MetalDevice = unretained(device) let content = String(cString: shader) let fileLocation = String(cString: file) do { let obsShader = try OBSShader(type: .vertex, content: content, fileLocation: fileLocation) let transpiled = try obsShader.transpiled() guard let metaData = obsShader.metaData else { OBSLog(.error, "device_vertexshader_create: No required metadata found for transpiled shader") return nil } let metalShader = try MetalShader(device: device, source: transpiled, type: .vertex, data: metaData) return metalShader.getRetained() } catch let error as ParserError { switch error { case .parseFail(let description): OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(description)") default: OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(error.description)") } } catch let error as ShaderError { switch error { case .transpileError(let description): OBSLog(.error, "device_vertexshader_create: Error transpiling shader.\n\(description)") case .parseError(let description): OBSLog(.error, "device_vertexshader_create: OBS parser error.\n\(description)") case .parseFail(let description): OBSLog(.error, "device_vertexshader_create: OBS parser failure.\n\(description)") default: OBSLog(.error, "device_vertexshader_create: OBS shader error.\n\(error.description)") } } catch { switch error { case let error as MetalShaderError: OBSLog(.error, "device_vertexshader_create: Error compiling shader.\n\(error.description)") case let error as MetalError.MTLDeviceError: OBSLog(.error, "device_vertexshader_create: Device error compiling shader.\n\(error.description)") default: OBSLog(.error, "device_vertexshader_create: Unknown error occurred") } } return nil } /// Creates a ``MetalShader`` instance from the given shader string for use as a fragment shader. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - shader: C character pointer with the contents of the `libobs` effect file /// - file: C character pointer with the contents of the `libobs` effect file location /// - error_string: Pointer for another C character pointer with the contents of an error description /// - Returns: Opaque pointer to a new ``MetalShader`` instance on success or `nil` on error /// /// The string pointed to by the `data` argument is a re-compiled shader string created from the associated "effect" /// file (which will contain multiple effects). Each effect is made up of several passes (though usually only a single /// pass is defined), each of which contains a vertex and fragment shader. This function is then called with just the /// vertex shader string. /// /// This fragment shader string needs to be parsed again and transpiled into a Metal shader string, which is handled by /// the ``OBSShader`` class. The transpiled string is then used to create the actual ``MetalShader`` instance. @_cdecl("device_pixelshader_create") public func device_pixelshader_create( device: UnsafeRawPointer, shader: UnsafePointer, file: UnsafePointer, error_string: UnsafeMutablePointer> ) -> OpaquePointer? { let device: MetalDevice = unretained(device) let content = String(cString: shader) let fileLocation = String(cString: file) do { let obsShader = try OBSShader(type: .fragment, content: content, fileLocation: fileLocation) let transpiled = try obsShader.transpiled() guard let metaData = obsShader.metaData else { OBSLog(.error, "device_pixelshader_create: No required metadata found for transpiled shader") return nil } let metalShader = try MetalShader(device: device, source: transpiled, type: .fragment, data: metaData) return metalShader.getRetained() } catch let error as ParserError { switch error { case .parseFail(let description): OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(description)") default: OBSLog(.error, "device_vertexshader_create: Error parsing shader.\n\(error.description)") } } catch let error as ShaderError { switch error { case .transpileError(let description): OBSLog(.error, "device_vertexshader_create: Error transpiling shader.\n\(description)") case .parseError(let description): OBSLog(.error, "device_vertexshader_create: OBS parser error.\n\(description)") case .parseFail(let description): OBSLog(.error, "device_vertexshader_create: OBS parser failure.\n\(description)") default: OBSLog(.error, "device_vertexshader_create: OBS shader error.\n\(error.description)") } } catch { switch error { case let error as MetalShaderError: OBSLog(.error, "device_vertexshader_create: Error compiling shader.\n\(error.description)") case let error as MetalError.MTLDeviceError: OBSLog(.error, "device_vertexshader_create: Device error compiling shader.\n\(error.description)") default: OBSLog(.error, "device_vertexshader_create: Unknown error occurred") } } return nil } /// Loads the ``MetalShader`` instance for use as the vertex shader for the current render pipeline descriptor. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - vertShader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// /// This function will simply set up the ``MTLFunction`` wrapped by the ``MetalShader`` instance as the current /// pipeline descriptor's `vertexFunction`. The Metal renderer will lazily create new render pipeline states for each /// permutation of pipeline descriptors, which is a comparatively costly operation but will only occur once for any /// such permutation. /// /// > Note: If a `NULL` pointer is passed for the `vertShader` argument, the vertex function on the current render /// pipeline descriptor will be _unset_. /// @_cdecl("device_load_vertexshader") public func device_load_vertexshader(device: UnsafeRawPointer, vertShader: UnsafeRawPointer?) { let device: MetalDevice = unretained(device) if let vertShader { let shader: MetalShader = unretained(vertShader) guard shader.type == .vertex else { assertionFailure("device_load_vertexshader: Invalid shader type \(shader.type)") return } device.renderState.vertexShader = shader device.renderState.pipelineDescriptor.vertexFunction = shader.function device.renderState.pipelineDescriptor.vertexDescriptor = shader.vertexDescriptor } else { device.renderState.vertexShader = nil device.renderState.pipelineDescriptor.vertexFunction = nil device.renderState.pipelineDescriptor.vertexDescriptor = nil } } /// Loads the ``MetalShader`` instance for use as the fragment shader for the current render pipeline descriptor. /// - Parameters: /// - device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - vertShader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// /// This function will simply set up the ``MTLFunction`` wrapped by the ``MetalShader`` instance as the current /// pipeline descriptor's `fragmentFunction`. The Metal renderer will lazily create new render pipeline states for /// each permutation of pipeline descriptors, which is a comparatively costly operation but will only occur once for /// any such permutation. /// /// As any fragment function is potentially associated with a number of textures and associated sampler states, the /// associated arrays are reset whenever a new fragment function is set up. /// /// > Note: If a `NULL` pointer is passed for the `pixelShader` argument, the fragment function on the current render /// pipeline descriptor will be _unset_. /// @_cdecl("device_load_pixelshader") public func device_load_pixelshader(device: UnsafeRawPointer, pixelShader: UnsafeRawPointer?) { let device: MetalDevice = unretained(device) for index in 0.. OpaquePointer? { let device: MetalDevice = unretained(device) if let shader = device.renderState.vertexShader { return shader.getUnretained() } else { return nil } } /// Gets the ``MetalShader`` set up as the current fragment shader for the pipeline /// - Parameter device: Opaque pointer to ``MetalDevice`` instance shared with `libobs` /// - Returns: Opaque pointer to ``MetalShader`` instance if a fragment shader is currently set up or `nil` otherwise @_cdecl("device_get_pixel_shader") public func device_get_pixel_shader(device: UnsafeRawPointer) -> OpaquePointer? { let device: MetalDevice = unretained(device) if let shader = device.renderState.fragmentShader { return shader.getUnretained() } else { return nil } } /// Requests the deinitialization of the ``MetalShader`` instance shared with `libobs` /// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// /// Ownership of the ``MetalShader`` instance will be transferred into the function and if this was the last strong /// reference to it, the object will be automatically deinitialized and deallocated by Swift. @_cdecl("gs_shader_destroy") public func gs_shader_destroy(shader: UnsafeRawPointer) { let _ = retained(shader) as MetalShader } /// Gets the number of uniform parameters used on the ``MetalShader`` /// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// - Returns: Number of uniforms @_cdecl("gs_shader_get_num_params") public func gs_shader_get_num_params(shader: UnsafeRawPointer) -> UInt32 { let shader: MetalShader = unretained(shader) return UInt32(shader.uniforms.count) } /// Gets a uniform parameter from the ``MetalShader`` by its array index /// - Parameters: /// - shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// - param: Array index of uniform parameter to get /// - Returns: Opaque pointer to a ``ShaderUniform`` instance if index within uniform array bounds or `nil` otherwise /// /// This function requires that the array indices of the uniforms array do not change for a ``MetalShader`` and also /// that the exact order of uniforms is identical between `libobs`'s interpretation of the effects file and the /// transpiled shader's analysis of the uniforms. /// /// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become /// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms /// array. @_cdecl("gs_shader_get_param_by_idx") public func gs_shader_get_param_by_idx(shader: UnsafeRawPointer, param: UInt32) -> OpaquePointer? { let shader: MetalShader = unretained(shader) guard param < shader.uniforms.count else { return nil } let uniform = shader.uniforms[Int(param)] let unretained = Unmanaged.passUnretained(uniform).toOpaque() return OpaquePointer(unretained) } /// Gets a uniform parameter from the ``MetalShader`` by its name /// - Parameters: /// - shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// - param: C character array pointer with the name of the requested uniform parameter /// - Returns: Opaque pointer to a ``ShaderUniform`` instance if any uniform with the provided name was found or `nil` /// otherwise /// /// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become /// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms /// array. /// @_cdecl("gs_shader_get_param_by_name") public func gs_shader_get_param_by_name(shader: UnsafeRawPointer, param: UnsafeMutablePointer) -> OpaquePointer? { let shader: MetalShader = unretained(shader) let paramName = String(cString: param) for uniform in shader.uniforms { if uniform.name == paramName { let unretained = Unmanaged.passUnretained(uniform).toOpaque() return OpaquePointer(unretained) } } return nil } /// Gets the uniform parameter associated with the view projection matrix used by the ``MetalShader`` /// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the view projection matrix was found /// or `nil` otherwise /// /// The uniform for the view projection matrix has the associated name `viewProj` in the Metal renderer, thus a /// name-based lookup is used to find the associated ``ShaderUniform`` instance. /// /// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become /// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms /// array. /// @_cdecl("gs_shader_get_viewproj_matrix") public func gs_shader_get_viewproj_matrix(shader: UnsafeRawPointer) -> OpaquePointer? { let shader: MetalShader = unretained(shader) let paramName = "viewProj" for uniform in shader.uniforms { if uniform.name == paramName { let unretained = Unmanaged.passUnretained(uniform).toOpaque() return OpaquePointer(unretained) } } return nil } /// Gets the uniform parameter associated with the world projection matrix used by the ``MetalShader`` /// - Parameter shader: Opaque pointer to ``MetalShader`` instance shared with `libobs` /// - Returns: Opaque pointer to a ``ShaderUniform`` instance if a uniform for the world projection matrix was found /// or `nil` otherwise /// /// The uniform for the view projection matrix has the associated name `worldProj` in the Metal renderer, thus a /// name-based lookup is used to find the associated ``ShaderUniform`` instance. /// /// > Important: The opaque pointer for the ``ShaderUniform`` instance is passe unretained and as such can become /// invalid when its owning ``MetalShader`` instance either is deinitialized itself or is replaced in the uniforms /// array. @_cdecl("gs_shader_get_world_matrix") public func gs_shader_get_world_matrix(shader: UnsafeRawPointer) -> OpaquePointer? { let shader: MetalShader = unretained(shader) let paramName = "worldProj" for uniform in shader.uniforms { if uniform.name == paramName { let unretained = Unmanaged.passUnretained(uniform).toOpaque() return OpaquePointer(unretained) } } return nil } /// Gets the name and uniform type from the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - info: Pointer to a `gs_shader_param_info` struct pre-allocated by `libobs` /// /// > Warning: The C character array pointer holding the name of the uniform is managed by Swift and might become /// invalid at any point in time. @_cdecl("gs_shader_get_param_info") public func gs_shader_get_param_info(shaderParam: UnsafeRawPointer, info: UnsafeMutablePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) shaderUniform.name.withCString { info.pointee.name = $0 } info.pointee.type = shaderUniform.gsType } /// Sets a boolean value on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: Boolean value to set for the uniform @_cdecl("gs_shader_set_bool") public func gs_shader_set_bool(shaderParam: UnsafeRawPointer, val: Bool) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) withUnsafePointer(to: val) { shaderUniform.setParameter(data: $0, size: MemoryLayout.size) } } /// Sets a 32-bit floating point value on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: 32-bit floating point value to set for the uniform @_cdecl("gs_shader_set_float") public func gs_shader_set_float(shaderParam: UnsafeRawPointer, val: Float32) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) withUnsafePointer(to: val) { shaderUniform.setParameter(data: $0, size: MemoryLayout.size) } } /// Sets a 32-bit signed integer value on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: 32-bit signed integer value to set for the uniform @_cdecl("gs_shader_set_int") public func gs_shader_set_int(shaderParam: UnsafeRawPointer, val: Int32) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) withUnsafePointer(to: val) { shaderUniform.setParameter(data: $0, size: MemoryLayout.size) } } /// Sets a 3x3 matrix of 32-bit floating point values on the ``ShaderUniform``instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A 3x3 matrix of 32-bit floating point values /// /// The 3x3 matrix is converted into a 4x4 matrix (padded with zeros) before actually being set as the uniform data @_cdecl("gs_shader_set_matrix3") public func gs_shader_set_matrix3(shaderParam: UnsafeRawPointer, val: UnsafePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) var newMatrix = matrix4() matrix4_from_matrix3(&newMatrix, val) shaderUniform.setParameter(data: &newMatrix, size: MemoryLayout.size) } /// Sets a 4x4 matrix of 32-bit floating point values on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A 4x4 matrix of 32-bit floating point values @_cdecl("gs_shader_set_matrix4") public func gs_shader_set_matrix4(shaderParam: UnsafeRawPointer, val: UnsafePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) shaderUniform.setParameter(data: val, size: MemoryLayout.size) } /// Sets a vector of 2 32-bit floating point values on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A vector of 2 32-bit floating point values @_cdecl("gs_shader_set_vec2") public func gs_shader_set_vec2(shaderParam: UnsafeRawPointer, val: UnsafePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) shaderUniform.setParameter(data: val, size: MemoryLayout.size) } /// Sets a vector of 3 32-bit floating point values on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A vector of 3 32-bit floating point values @_cdecl("gs_shader_set_vec3") public func gs_shader_set_vec3(shaderParam: UnsafeRawPointer, val: UnsafePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) shaderUniform.setParameter(data: val, size: MemoryLayout.size) } /// Sets a vector of 4 32-bit floating point values on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A vector of 4 32-bit floating point values @_cdecl("gs_shader_set_vec4") public func gs_shader_set_vec4(shaderParam: UnsafeRawPointer, val: UnsafePointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) shaderUniform.setParameter(data: val, size: MemoryLayout.size) } /// Sets up the data of a `gs_shader_texture` struct as a uniform on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: A pointer to a `gs_shader_struct` containing an opaque pointer to the actual ``MetalTexture`` instance /// and an sRGB gamma state flag /// /// The struct's data is copied verbatim into the uniform, which allows reconstruction of the pointer at a later point /// as long as the actual ``MetalTexture`` instance still exists. @_cdecl("gs_shader_set_texture") public func gs_shader_set_texture(shaderParam: UnsafeRawPointer, val: UnsafePointer?) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) if let val { shaderUniform.setParameter(data: val, size: MemoryLayout.size) } } /// Sets an arbitrary value on the ``ShaderUniform`` instance /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - val: Opaque pointer to some unknown data for use as the uniform /// - size: The size of the data available at the memory pointed to by the `val` argument /// /// The ``ShaderUniform`` itself is set up to hold a specific uniform type, each of which is associated with a size of /// bytes required for it. If the size of the data pointed to by `val` does not fit into this size, the uniform will /// not be updated. /// /// If the ``ShaderUniform`` expects a texture parameter, the pointer will be bound as memory of a `gs_shader_texture` /// instance before setting it up. @_cdecl("gs_shader_set_val") public func gs_shader_set_val(shaderParam: UnsafeRawPointer, val: UnsafeRawPointer, size: UInt32) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) let size = Int(size) let valueSize = shaderUniform.gsType.size guard valueSize == size else { assertionFailure("gs_shader_set_val: Required size of uniform does not match size of input") return } if shaderUniform.gsType == GS_SHADER_PARAM_TEXTURE { let shaderTexture = val.bindMemory(to: gs_shader_texture.self, capacity: 1) shaderUniform.setParameter(data: shaderTexture, size: valueSize) } else { let bytes = val.bindMemory(to: UInt8.self, capacity: valueSize) shaderUniform.setParameter(data: bytes, size: valueSize) } } /// Resets the ``ShaderUniform``'s current data with its default data /// - Parameter shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// /// Each ``ShaderUniform`` is optionally set up with a set of default data (stored as an array of bytes) which is /// simply copied into the current values. @_cdecl("gs_shader_set_default") public func gs_shader_set_default(shaderParam: UnsafeRawPointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) if let defaultValues = shaderUniform.defaultValues { shaderUniform.currentValues = Array(defaultValues) } } /// Sets up the ``MTLSamplerState`` as the sampler state for the ``ShaderUniform`` /// - Parameters: /// - shaderParam: Opaque pointer to ``ShaderUniform`` instance shared with `libobs` /// - sampler: Opaque pointer to ``MTLSamplerState`` instance shared with `libobs` /// /// If the uniform represents a texture for use in the associated shader, this function will also set up the provided /// ``MTLSamplerState`` for the associated texture's texture slot. @_cdecl("gs_shader_set_next_sampler") public func gs_shader_set_next_sampler(shaderParam: UnsafeRawPointer, sampler: UnsafeRawPointer) { let shaderUniform: MetalShader.ShaderUniform = unretained(shaderParam) let samplerState = Unmanaged.fromOpaque(sampler).takeUnretainedValue() shaderUniform.samplerState = samplerState }