mirror of
https://github.com/OpenEmu/OpenEmuKit.git
synced 2025-11-01 11:08:14 +00:00
322 lines
12 KiB
Swift
322 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 Foundation
|
|
import OpenEmuBase
|
|
import OpenEmuShaders
|
|
|
|
@objc
|
|
public class OEShadersModel : NSObject {
|
|
// MARK: Notifications
|
|
|
|
@objc public static let shaderModelCustomShadersDidChange = Notification.Name("OEShaderModelCustomShadersDidChangeNotification")
|
|
|
|
public enum Preferences {
|
|
case global
|
|
case system(String)
|
|
|
|
public var key: String {
|
|
get {
|
|
switch self {
|
|
case .global:
|
|
return "videoShader"
|
|
case .system(let identifier):
|
|
return "videoShader.\(identifier)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let store: UserDefaults
|
|
private let userPathName: String
|
|
private let bundle: Bundle
|
|
|
|
private var systemShaders = [OEShaderModel]()
|
|
private var customShaders = [OEShaderModel]()
|
|
|
|
|
|
/// Creates a shader model used for accessing shaders and their user state.
|
|
/// - Parameters:
|
|
/// - store: The user defaults store to read and write to.
|
|
/// - bundle: The main bundle used to locate shaders.
|
|
/// - name: The name of the path used to read and write user-specified shaders,
|
|
/// from within the application support directory. A `nil`
|
|
/// value will use the `kCFBundleNameKey` from the main bundle; otherwise, `OpenEmuKit` will be used.
|
|
@objc public init(store: UserDefaults, bundle: Bundle = .main, userPathName name: String? = nil) {
|
|
self.store = store
|
|
self.bundle = bundle
|
|
self.userPathName = name ?? bundle.infoDictionary?[kCFBundleNameKey as String] as? String ?? "OpenEmuKit"
|
|
super.init()
|
|
|
|
self.systemShaders = loadSystemShaders()
|
|
self.customShaders = loadCustomShaders()
|
|
}
|
|
|
|
@objc public func reload() {
|
|
customShaders = loadCustomShaders()
|
|
_allShaderNames = nil
|
|
_customShaderNames = nil
|
|
NotificationCenter.default.post(name: Self.shaderModelCustomShadersDidChange, object: nil)
|
|
}
|
|
|
|
private var _systemShaderNames: [String]?
|
|
|
|
@objc public var systemShaderNames: [String] {
|
|
if _systemShaderNames == nil {
|
|
_systemShaderNames = systemShaders.map(\.name)
|
|
}
|
|
return _systemShaderNames!
|
|
}
|
|
|
|
private var _customShaderNames: [String]?
|
|
|
|
@objc public var customShaderNames: [String] {
|
|
if _customShaderNames == nil {
|
|
_customShaderNames = customShaders.map(\.name)
|
|
}
|
|
return _customShaderNames!
|
|
}
|
|
|
|
private var _allShaderNames: [String]?
|
|
|
|
@objc public var allShaderNames: [String] {
|
|
if _allShaderNames == nil {
|
|
|
|
}
|
|
return _allShaderNames!
|
|
}
|
|
|
|
@objc public var defaultShader: OEShaderModel {
|
|
get {
|
|
if let name = store.string(forKey: Preferences.global.key),
|
|
let shader = self[name] {
|
|
return shader
|
|
}
|
|
|
|
return self["Pixellate"]!
|
|
}
|
|
|
|
set {
|
|
store.set(newValue.name, forKey: Preferences.global.key)
|
|
}
|
|
}
|
|
|
|
@objc public func shader(withName name: String) -> OEShaderModel? {
|
|
return self[name]
|
|
}
|
|
|
|
@objc public func shader(forSystem identifier: String) -> OEShaderModel? {
|
|
guard let name = store.string(forKey: Preferences.system(identifier).key) else {
|
|
return defaultShader
|
|
}
|
|
return self[name]
|
|
}
|
|
|
|
@objc public func shader(forURL url: URL) -> OEShaderModel? {
|
|
return OEShaderModel(url: url, store: store)
|
|
}
|
|
|
|
subscript(name: String) -> OEShaderModel? {
|
|
return systemShaders.first(where: { $0.name == name }) ?? customShaders.first(where: { $0.name == name })
|
|
}
|
|
|
|
// MARK: - helpers
|
|
|
|
private func loadSystemShaders() -> [OEShaderModel] {
|
|
if let path = bundle.resourcePath {
|
|
let url = URL(fileURLWithPath: path, isDirectory: true).appendingPathComponent("Shaders", isDirectory: true)
|
|
let urls = Self.urlsForShaders(at: url)
|
|
return urls.map { OEShaderModel(url: $0, store: store) }
|
|
}
|
|
return []
|
|
}
|
|
|
|
private func loadCustomShaders() -> [OEShaderModel] {
|
|
guard let path = userShadersPath else { return [] }
|
|
return Self.urlsForShaders(at: path).map { OEShaderModel(url: $0, store: store) }
|
|
}
|
|
|
|
private static func urlsForShaders(at url: URL) -> [URL] {
|
|
var res = [URL]()
|
|
|
|
let fm = FileManager.default
|
|
|
|
guard
|
|
let urls = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsSubdirectoryDescendants)
|
|
else { return [] }
|
|
|
|
let dirs = urls.filter({ (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false })
|
|
for dir in dirs {
|
|
guard
|
|
let files = try? fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
|
|
else { continue }
|
|
if let slangp = files.first(where: { $0.pathExtension == "slangp" }) {
|
|
// we have a file!
|
|
res.append(slangp)
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
@objc public var userShadersPath: URL? {
|
|
guard
|
|
let path = try? FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
|
else { return nil }
|
|
|
|
return path.appendingPathComponent(userPathName, isDirectory: true).appendingPathComponent("Shaders", isDirectory: true)
|
|
}
|
|
|
|
@objc public var shadersCachePath: URL? {
|
|
guard
|
|
let path = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
|
else { return nil }
|
|
|
|
return path.appendingPathComponent(userPathName, isDirectory: true).appendingPathComponent("Shaders", isDirectory: true)
|
|
}
|
|
|
|
// MARK: - Shader Model
|
|
|
|
@objc(OEShaderModel)
|
|
@objcMembers
|
|
public class OEShaderModel: NSObject {
|
|
public enum Params {
|
|
case global(String)
|
|
case system(String, String)
|
|
|
|
public var key: String {
|
|
get {
|
|
switch self {
|
|
case .global(let shader):
|
|
return "videoShader.\(shader).params"
|
|
case .system(let shader, let identifier):
|
|
return "videoShader.\(identifier).\(shader).params"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let store: UserDefaults
|
|
public var name: String
|
|
public var url: URL
|
|
|
|
init(url: URL, store: UserDefaults) {
|
|
self.store = store
|
|
self.name = url.deletingLastPathComponent().lastPathComponent
|
|
self.url = url
|
|
}
|
|
|
|
@objc
|
|
public func parameters(forIdentifier identifier: String) -> [String: Double]? {
|
|
if let state = store.string(forKey: Params.system(self.name, identifier).key) {
|
|
var res = [String:Double]()
|
|
for param in state.split(separator: ";") {
|
|
let vals = param.split(separator: "=")
|
|
if let d = Double(vals[1]) {
|
|
res[String(vals[0])] = d
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
@objc
|
|
public func write(parameters params: [OEShaderParamValue], identifier: String) {
|
|
var state = [String]()
|
|
|
|
for p in params.filter({ !$0.isInitial }) {
|
|
state.append("\(p.name)=\(p.value)")
|
|
}
|
|
if state.isEmpty {
|
|
store.removeObject(forKey: Params.system(self.name, identifier).key)
|
|
} else {
|
|
store.set(state.joined(separator: ";"), forKey: Params.system(self.name, identifier).key)
|
|
}
|
|
}
|
|
|
|
override public var description: String {
|
|
return self.name
|
|
}
|
|
|
|
public override var debugDescription: String {
|
|
return "\(name) \(url.absoluteString)"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OEShadersModel.OEShaderModel {
|
|
public func readGroups() -> [OEShaderParamGroupValue] {
|
|
guard let ss = try? SlangShader(fromURL: url) else { return [] }
|
|
|
|
if let groups = readGroupsModel() {
|
|
var all = ss.parameters
|
|
var dg: OEShaderParamGroupValue?
|
|
|
|
let res: [OEShaderParamGroupValue] = groups.enumerated().map { (i, g) in
|
|
let gv = OEShaderParamGroupValue()
|
|
gv.index = i
|
|
gv.name = g.name
|
|
gv.desc = g.desc
|
|
gv.hidden = g.hidden
|
|
|
|
if g.name == "default" {
|
|
dg = gv
|
|
}
|
|
|
|
// return a list of parameters from SlangShader in same order as g.parameters
|
|
let p = g.parameters.compactMap { name in
|
|
all.first { $0.name == name }
|
|
}
|
|
all.removeAll { p.contains($0) }
|
|
|
|
gv.parameters = OEShaderParamValue.withParameters(p)
|
|
return gv
|
|
}
|
|
|
|
if let dg = dg, !all.isEmpty {
|
|
dg.parameters = OEShaderParamValue.withParameters(all)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
let gv = OEShaderParamGroupValue()
|
|
gv.index = 0
|
|
gv.name = "default"
|
|
gv.desc = "Default"
|
|
gv.parameters = OEShaderParamValue.withParameters(ss.parameters)
|
|
return [gv]
|
|
}
|
|
|
|
func readGroupsModel() -> [ShaderParameterGroupModel]? {
|
|
let groupsURL = url.deletingLastPathComponent().appendingPathComponent("parameterGroups.plist")
|
|
guard let data = try? Data(contentsOf: groupsURL) else { return nil }
|
|
|
|
let dec = PropertyListDecoder()
|
|
return try? dec.decode([ShaderParameterGroupModel].self, from: data)
|
|
}
|
|
}
|