mirror of
https://github.com/OpenEmu/OpenEmu-Shaders.git
synced 2025-11-01 11:07:54 +00:00
328 lines
12 KiB
Swift
328 lines
12 KiB
Swift
// Copyright (c) 2019, 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 CommonCrypto
|
|
import Foundation
|
|
import Metal
|
|
|
|
enum SourceParserError: LocalizedError {
|
|
case missingVersion
|
|
case multipleFormatPragma
|
|
case multipleNamePragma
|
|
case duplicateParameterPragma
|
|
case includeNotFound
|
|
case includeFileNotFound(String)
|
|
case invalidParameterPragma
|
|
case invalidFormatPragma
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .missingVersion:
|
|
return NSLocalizedString("Root slang shader missing #version",
|
|
comment: "The slang file is missing the required #version directive")
|
|
case .multipleFormatPragma:
|
|
return NSLocalizedString("#pragma format declared multiple times",
|
|
comment: "The slang file contains multiple declarations of the #pragma format directive")
|
|
case .multipleNamePragma:
|
|
return NSLocalizedString("#pragma name declared multiple times",
|
|
comment: "The slang file contains multiple declarations of the #pragma name directive")
|
|
case .duplicateParameterPragma:
|
|
return NSLocalizedString("duplicate #pragma parameter",
|
|
comment: "The slang file contains duplicate #pragma parameter directives")
|
|
case .includeNotFound:
|
|
return NSLocalizedString("#include not found",
|
|
comment: "The slang file has an invalid #include directive")
|
|
case .includeFileNotFound(let filename):
|
|
return String(format: NSLocalizedString("#include file not found: %@",
|
|
comment: "The slang file has an invalid #include directive"), filename)
|
|
case .invalidParameterPragma:
|
|
return NSLocalizedString("invalid #pragma parameter declaration",
|
|
comment: "The slang file contains an invalid parameter directive")
|
|
case .invalidFormatPragma:
|
|
return NSLocalizedString("invalid #pragma format",
|
|
comment: "The slang file contains an invalid #pragma format directive")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
mutating func replaceOccurrences(of target: String, with replacement: String, options: String.CompareOptions = [], locale: Locale? = nil) {
|
|
var range: Range<String.Index>?
|
|
while let r = self.range(of: target, options: options, range: range.map { $0.lowerBound..<self.endIndex }, locale: locale) {
|
|
replaceSubrange(r, with: replacement)
|
|
range = r
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Sequence<UInt8> {
|
|
var base64: String {
|
|
var res = Data(self).base64EncodedString()
|
|
res.replaceOccurrences(of: "+", with: "-")
|
|
res.replaceOccurrences(of: "/", with: "_")
|
|
res.replaceOccurrences(of: "=", with: "")
|
|
return res
|
|
}
|
|
}
|
|
|
|
/**
|
|
* OESourceParser is responsible for parsing the .slang source file from the provided url.
|
|
*
|
|
* Valid `#pragma` directives include `name`, `format` and `parameter`.
|
|
*/
|
|
class SourceParser {
|
|
private var buffer: [String]
|
|
var parametersMap: [String: ShaderParameter]
|
|
var included: Set<String> = Set()
|
|
|
|
private(set) var name: String?
|
|
|
|
let basename: String
|
|
|
|
var parameters: [ShaderParameter]
|
|
|
|
var format: Compiled.PixelFormat?
|
|
|
|
lazy var sha256: String = {
|
|
let data = Array(buffer.joined().utf8)
|
|
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
data.withUnsafeBytes {
|
|
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest)
|
|
}
|
|
return digest.base64
|
|
}()
|
|
|
|
/**
|
|
Returns the vertex shader portion of the slang file
|
|
*/
|
|
lazy var vertexSource: String = self.findSource(forStage: "vertex")
|
|
|
|
/**
|
|
Returns the fragment shader portion of the slang file
|
|
*/
|
|
lazy var fragmentSource: String = self.findSource(forStage: "fragment")
|
|
|
|
init(fromURL url: URL) throws {
|
|
buffer = []
|
|
parametersMap = [:]
|
|
parameters = []
|
|
format = nil
|
|
basename = (url.lastPathComponent as NSString).deletingPathExtension
|
|
|
|
try autoreleasepool {
|
|
try self.load(url, isRoot: true)
|
|
}
|
|
}
|
|
|
|
private func findSource(forStage: String) -> String {
|
|
var src: [String] = []
|
|
var keep = true
|
|
for line in buffer {
|
|
if line.hasPrefix(Prefixes.pragmaStage) {
|
|
let s = Scanner(string: line)
|
|
s.scanString(Prefixes.pragmaStage, into: nil)
|
|
var tmp: NSString?
|
|
s.scanCharacters(from: .alphanumerics, into: &tmp)
|
|
guard let stage = tmp as String? else {
|
|
continue
|
|
}
|
|
keep = stage == forStage
|
|
} else if line.hasPrefix(Prefixes.pragmaName) || line.hasPrefix(Prefixes.pragmaFormat) {
|
|
// skip
|
|
} else if keep {
|
|
src.append(line)
|
|
}
|
|
}
|
|
|
|
return src.joined(separator: "\n")
|
|
}
|
|
|
|
// swiftformat:disable consecutiveSpaces
|
|
|
|
enum Prefixes {
|
|
static let version = "#version "
|
|
static let include = "#include "
|
|
static let endif = "#endif"
|
|
static let pragma = "#pragma "
|
|
static let pragmaName = "#pragma name "
|
|
static let pragmaParam = "#pragma parameter "
|
|
static let pragmaFormat = "#pragma format "
|
|
static let pragmaStage = "#pragma stage "
|
|
}
|
|
|
|
// swiftformat:enable all
|
|
|
|
private func load(_ url: URL, isRoot: Bool) throws {
|
|
let f = try String(contentsOf: url)
|
|
let filename = url.lastPathComponent
|
|
var lines = [String]()
|
|
f.enumerateLines { line, _ in lines.append(line) }
|
|
|
|
var lno = 1
|
|
|
|
var oe = lines.makeIterator()
|
|
if isRoot {
|
|
guard let line = oe.next(), line.hasPrefix(Prefixes.version) else {
|
|
throw SourceParserError.missingVersion
|
|
}
|
|
buffer.append(line)
|
|
buffer.append("#extension GL_GOOGLE_cpp_style_line_directive : require")
|
|
lno += 1
|
|
}
|
|
|
|
buffer.append("#line \(lno) \"\(filename)\"")
|
|
|
|
for line in oe {
|
|
if line.hasPrefix(Prefixes.include) {
|
|
let s = Scanner(string: line)
|
|
s.scanString(Prefixes.include, into: nil)
|
|
guard let filepath = s.scanQuotedString() else {
|
|
throw SourceParserError.includeNotFound
|
|
}
|
|
let file = URL(string: filepath, relativeTo: url.deletingLastPathComponent())!
|
|
if file.isFileURL, !FileManager.default.fileExists(atPath: file.path) {
|
|
throw SourceParserError.includeFileNotFound(file.path)
|
|
}
|
|
if !included.contains(file.absoluteString) {
|
|
try load(file, isRoot: false)
|
|
buffer.append("#line \(lno) \"\(filename)\"")
|
|
included.insert(file.absoluteString)
|
|
}
|
|
} else {
|
|
let hasPreprocessor: Bool
|
|
if line.hasPrefix(Prefixes.pragma) {
|
|
hasPreprocessor = true
|
|
if try processPragma(line: line) {
|
|
// skip line
|
|
continue
|
|
}
|
|
} else if line.hasPrefix(Prefixes.endif) {
|
|
hasPreprocessor = true
|
|
} else {
|
|
hasPreprocessor = false
|
|
}
|
|
|
|
buffer.append(line)
|
|
|
|
if hasPreprocessor {
|
|
buffer.append("#line \(lno + 1) \"\(filename)\"")
|
|
}
|
|
}
|
|
lno += 1
|
|
}
|
|
}
|
|
|
|
static let identifierCharacters = { () -> CharacterSet in
|
|
var set = CharacterSet.alphanumerics
|
|
set.formUnion(CharacterSet(charactersIn: "_"))
|
|
return set as CharacterSet
|
|
}
|
|
|
|
private func processPragma(line: String) throws -> Bool {
|
|
if line.hasPrefix(Prefixes.pragmaName) {
|
|
if name != nil {
|
|
throw SourceParserError.multipleNamePragma
|
|
}
|
|
|
|
name = String(line.dropFirst(Prefixes.pragmaName.count))
|
|
} else if line.hasPrefix(Prefixes.pragmaParam) {
|
|
let s = Scanner(string: line)
|
|
s.scanString(Prefixes.pragmaParam, into: nil)
|
|
|
|
var count = 0
|
|
var tmp: NSString?
|
|
count += s.scanCharacters(from: .identifierCharacters, into: &tmp) ? 1 : 0
|
|
|
|
guard let name = tmp as String? else {
|
|
throw SourceParserError.invalidParameterPragma
|
|
}
|
|
|
|
guard let desc = s.scanQuotedString() else {
|
|
throw SourceParserError.invalidParameterPragma
|
|
}
|
|
count += 1
|
|
var initial: Decimal = 0, minimum: Decimal = 0, maximum: Decimal = 0, step: Decimal = 0
|
|
count += s.scanDecimal(&initial) ? 1 : 0
|
|
count += s.scanDecimal(&minimum) ? 1 : 0
|
|
count += s.scanDecimal(&maximum) ? 1 : 0
|
|
count += s.scanDecimal(&step) ? 1 : 0
|
|
|
|
if count == 5 {
|
|
step = 0.1 * (maximum - minimum)
|
|
count += 1
|
|
}
|
|
|
|
if count == 6 {
|
|
let param = ShaderParameter(name: name, desc: desc)
|
|
param.initial = initial
|
|
param.minimum = minimum
|
|
param.maximum = maximum
|
|
param.step = step
|
|
|
|
if let existing = parametersMap[name], param != existing {
|
|
throw SourceParserError.duplicateParameterPragma
|
|
}
|
|
|
|
parametersMap[name] = param
|
|
parameters.append(param)
|
|
} else {
|
|
throw SourceParserError.invalidParameterPragma
|
|
}
|
|
} else if line.hasPrefix(Prefixes.pragmaFormat) {
|
|
if format != nil {
|
|
throw SourceParserError.invalidParameterPragma
|
|
}
|
|
|
|
let s = Scanner(string: line)
|
|
s.scanString(Prefixes.pragmaFormat, into: nil)
|
|
var tmp: NSString?
|
|
s.scanCharacters(from: .identifierCharacters, into: &tmp)
|
|
if let fmt = tmp as String? {
|
|
format = .init(glslangFormat: fmt)
|
|
}
|
|
if format == nil {
|
|
throw SourceParserError.invalidFormatPragma
|
|
}
|
|
}
|
|
|
|
return !line.hasPrefix(Prefixes.pragmaStage)
|
|
}
|
|
}
|
|
|
|
extension CharacterSet {
|
|
static var identifierCharacters: CharacterSet = {
|
|
var set = CharacterSet.alphanumerics
|
|
set.formUnion(CharacterSet(charactersIn: "_"))
|
|
return set
|
|
}()
|
|
|
|
static var doubleQuotes: CharacterSet = .init(charactersIn: "\"")
|
|
|
|
static var whitespacesAndComment: CharacterSet = {
|
|
var set = CharacterSet.whitespaces
|
|
set.formUnion(CharacterSet(charactersIn: "#"))
|
|
return set
|
|
}()
|
|
}
|