Skip to content

Instantly share code, notes, and snippets.

@nkallen
Last active December 15, 2019 22:02
Show Gist options
  • Save nkallen/90054ac97d0e0798b778ac73a84d8d3f to your computer and use it in GitHub Desktop.
Save nkallen/90054ac97d0e0798b778ac73a84d8d3f to your computer and use it in GitHub Desktop.
//
// Created by Krzysztof Zablocki on 06/01/2017.
// Copyright (c) 2017 Pixle. All rights reserved.
//
import Foundation
extension Array {
func parallelFlatMap<T>(transform: (Element) throws -> [T]) throws -> [T] {
return try parallelMap(transform).flatMap { $0 }
}
/// We have to roll our own solution because concurrentPerform will use slowPath if no NSApplication is available
func parallelMap<T>(_ transform: (Element) throws -> T, progress: ((Int) -> Void)? = nil) throws -> [T] {
let count = self.count
let maxConcurrentJobs = ProcessInfo.processInfo.activeProcessorCount
guard count > 1 && maxConcurrentJobs > 1 else {
// skip GCD overhead if we'd only run one at a time anyway
return try map(transform)
}
var result = [(Int, [T])]()
result.reserveCapacity(count)
let group = DispatchGroup()
let uuid = NSUUID().uuidString
let jobCount = Int(ceil(Double(count) / Double(maxConcurrentJobs)))
let queueLabelPrefix = "io.pixle.Sourcery.map.\(uuid)"
let resultAccumulatorQueue = DispatchQueue(label: "\(queueLabelPrefix).resultAccumulator")
var mapError: Error?
withoutActuallyEscaping(transform) { escapingtransform in
for jobIndex in stride(from: 0, to: count, by: jobCount) {
let queue = DispatchQueue(label: "\(queueLabelPrefix).\(jobIndex / jobCount)")
queue.async(group: group) {
let jobElements = self[jobIndex..<Swift.min(count, jobIndex + jobCount)]
do {
let jobIndexAndResults = try (jobIndex, jobElements.map(escapingtransform))
resultAccumulatorQueue.sync {
result.append(jobIndexAndResults)
}
} catch {
resultAccumulatorQueue.sync {
mapError = error
}
}
}
}
group.wait()
}
if let mapError = mapError {
throw mapError
}
return result.sorted { $0.0 < $1.0 }.flatMap { $0.1 }
}
}
extension Path {
public var isMetalSourceFile: Bool {
return !self.isDirectory && self.extension == "metal"
}
public var isMetalLibFile: Bool {
return !self.isDirectory && self.extension == "metallib"
}
}
//
// Path+Extensions.swift
// Sourcery
//
// Created by Krunoslav Zaher on 1/6/17.
// Copyright © 2017 Pixle. All rights reserved.
//
public typealias Path = PathKit.Path
extension Path {
/// - returns: The `.cachesDirectory` search path in the user domain, as a `Path`.
public static var defaultBaseCachePath: Path {
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) as [String]
let path = paths[0]
return Path(path)
}
/// - parameter _basePath: The value of the `--cachePath` command line parameter, if any.
/// - note: This function does not consider the `--disableCache` command line parameter.
/// It is considered programmer error to call this function when `--disableCache` is specified.
public static func cachesDir(sourcePath: Path, basePath: Path? = nil, createIfMissing: Bool = true) -> Path {
let basePath = basePath ?? defaultBaseCachePath
let path = basePath + "MetalSmith" + sourcePath.lastComponent
if !path.exists && createIfMissing {
// swiftlint:disable:next force_try
try! FileManager.default.createDirectory(at: path.url, withIntermediateDirectories: true, attributes: nil)
}
return path
}
public var isTemplateFile: Bool {
return self.extension == "stencil" ||
self.extension == "swifttemplate" ||
self.extension == "ejs"
}
public var isSwiftSourceFile: Bool {
return !self.isDirectory && self.extension == "swift"
}
public func hasExtension(as string: String) -> Bool {
let extensionString = ".\(string)."
return self.string.contains(extensionString)
}
public init(_ string: String, relativeTo relativePath: Path) {
var path = Path(string)
if !path.isAbsolute {
path = (relativePath + path).absolute()
}
self.init(path.string)
}
public var allPaths: [Path] {
if isDirectory {
return (try? recursiveChildren()) ?? []
} else {
return [self]
}
}
func attributes() throws -> [FileAttributeKey : Any] {
return try FileManager.default.attributesOfItem(atPath: self.string)
}
subscript(attribute: FileAttributeKey) -> Any? {
do {
let attrs = try attributes()
return attrs[attribute]
} catch {
return nil
}
}
}
//
// FolderWatcher.swift
// Sourcery
//
// Created by Krzysztof Zabłocki on 24/12/2016.
// Copyright © 2016 Pixle. All rights reserved.
//
import Foundation
#if os(OSX)
public enum FolderWatcher {
public struct Event {
public let path: String
public let flag: Flag
public struct Flag: OptionSet {
public let rawValue: FSEventStreamEventFlags
public init(rawValue: FSEventStreamEventFlags) {
self.rawValue = rawValue
}
init(_ value: Int) {
self.rawValue = FSEventStreamEventFlags(value)
}
public static let isDirectory = Flag(kFSEventStreamEventFlagItemIsDir)
public static let isFile = Flag(kFSEventStreamEventFlagItemIsFile)
public static let created = Flag(kFSEventStreamEventFlagItemCreated)
public static let modified = Flag(kFSEventStreamEventFlagItemModified)
public static let removed = Flag(kFSEventStreamEventFlagItemRemoved)
public static let renamed = Flag(kFSEventStreamEventFlagItemRenamed)
public static let isHardlink = Flag(kFSEventStreamEventFlagItemIsHardlink)
public static let isLastHardlink = Flag(kFSEventStreamEventFlagItemIsLastHardlink)
public static let isSymlink = Flag(kFSEventStreamEventFlagItemIsSymlink)
public static let changeOwner = Flag(kFSEventStreamEventFlagItemChangeOwner)
public static let finderInfoModified = Flag(kFSEventStreamEventFlagItemFinderInfoMod)
public static let inodeMetaModified = Flag(kFSEventStreamEventFlagItemInodeMetaMod)
public static let xattrsModified = Flag(kFSEventStreamEventFlagItemXattrMod)
public var description: String {
var names: [String] = []
if self.contains(.isDirectory) { names.append("isDir") }
if self.contains(.isFile) { names.append("isFile") }
if self.contains(.created) { names.append("created") }
if self.contains(.modified) { names.append("modified") }
if self.contains(.removed) { names.append("removed") }
if self.contains(.renamed) { names.append("renamed") }
if self.contains(.isHardlink) { names.append("isHardlink") }
if self.contains(.isLastHardlink) { names.append("isLastHardlink") }
if self.contains(.isSymlink) { names.append("isSymlink") }
if self.contains(.changeOwner) { names.append("changeOwner") }
if self.contains(.finderInfoModified) { names.append("finderInfoModified") }
if self.contains(.inodeMetaModified) { names.append("inodeMetaModified") }
if self.contains(.xattrsModified) { names.append("xattrsModified") }
return names.joined(separator: ", ")
}
}
}
public class Local {
private let path: String
private var stream: FSEventStreamRef!
private let closure: (_ events: [Event]) -> Void
/// Creates folder watcher.
///
/// - Parameters:
/// - path: Path to observe
/// - latency: Latency to use
/// - closure: Callback closure
public init(path: String, latency: TimeInterval = 1/60, closure: @escaping (_ events: [Event]) -> Void) {
self.path = path
self.closure = closure
func handler(_ stream: ConstFSEventStreamRef, clientCallbackInfo: UnsafeMutableRawPointer?, numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) {
let eventStream = unsafeBitCast(clientCallbackInfo, to: Local.self)
let paths = unsafeBitCast(eventPaths, to: NSArray.self)
let events = (0..<numEvents).compactMap { idx in
return (paths[idx] as? String).flatMap { Event(path: $0, flag: Event.Flag(rawValue: eventFlags[idx])) }
}
eventStream.closure(events)
}
var context = FSEventStreamContext()
context.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
stream = FSEventStreamCreate(nil, handler, &context, [path] as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), latency, flags)
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
FSEventStreamStart(stream)
}
deinit {
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
}
#endif
public class MetalEnvironment: ObservableObject {
@Published public var device: MTLDevice?
@Published public var library: MTLLibrary?
@Published public var commandQueue: MTLCommandQueue?
var watcher: [FolderWatcher.Local]? = nil
func watch(_ paths: Paths) -> Self {
let source = paths.filter { $0.isMetalSourceFile }
let compiler = MetalCompiler()
self.watcher = source.process() { result in
if let sources = try? result.get(),
let metallib = try? compiler.compileAndArchive(sources) {
self.library = try? self.device?.makeLibrary(filepath: metallib.string)
}
}
return self
}
}
// Create it like:
MetalEnvironment().watch(Paths(include: ["/Users/nickkallen/Documents/SwiftMetal"], exclude: []))
// For example, this is what I do
struct ContentView: View {
@EnvironmentObject var environment: MetalEnvironment
var body: some View {
let result = environment.device?.makeTexture(width: 480, height: 640, usage: [.shaderRead, .shaderWrite])
return Group {
CommandBuffer() {
Colors(time: 0, result: result)
.dispatch(width: 480, height: 640)
}
Texture(result)
}
}
}
struct ContentView_Previews: PreviewProvider {
static let environment = MetalEnvironment()
.watch(Paths(include: ["/Users/XXX/Documents/YYY"], exclude: []))
static var previews: some View {
return ContentView()
.environmentObject(environment)
.previewLayout(.sizeThatFits)
}
}
import Foundation
import Logging
import PathKit
typealias CompilationResult = [Path]
let log = Logger(label: "com.nk.MetalSmith")
public class MetalCompiler {
let cachesPath: Path
#if os(iOS) || os(watchOS) || os(tvOS)
let sdk = "iphoneos"
#elseif os(OSX)
let sdk = "macosx"
#endif
public init(cachesPath: Path = Path.cachesDir(sourcePath: Path("MetalSmith"))) {
self.cachesPath = cachesPath
}
public func compileAndArchive(_ sources: [Path]) throws -> Path? {
var previousUpdate = 0
var accumulator = 0
let step = sources.count / 10 // every 10%
let airs = try sources.parallelMap({ try compile($0) }) { _ in
if accumulator > previousUpdate + step {
previousUpdate = accumulator
let percentage = accumulator * 100 / sources.count
log.info("Scanning sources... \(percentage)% (\(sources.count) files)")
}
accumulator += 1
}
return try archive(airs)
}
public func compile(_ in: Path) throws -> Path {
let out = cachesPath + Path("\(`in`.lastComponentWithoutExtension).air")
guard out.exists else {
log.info("Initial compilation of \(`in`.string).")
_ = shell("xcrun -sdk \(sdk) metal -c \(`in`.string) -o \(out.string)")
return out
}
if let inDate = `in`[.modificationDate] as? Date,
let outDate = out[.modificationDate] as? Date,
outDate < inDate {
log.info("Re-compiling \(`in`.string).")
_ = shell("xcrun -sdk \(sdk) metal -c \(`in`.string) -o \(out.string)")
} else {
log.info("Compiled version of \(`in`.string) is already up-to-date; skipping compilation.")
}
return out
}
private var defeatCache = 0 // device.makeLibrary() has an internal cache, so we cannot re-use filenames
public func archive(_ files: [Path]) throws -> Path? {
guard !files.isEmpty else { return nil }
let start = CFAbsoluteTimeGetCurrent()
if defeatCache > 0 {
let old = cachesPath + Path("default\(defeatCache-1).metallib")
if old.exists {
try old.delete()
}
}
let out = cachesPath + Path("default\(defeatCache).metallib")
_ = shell("xcrun -sdk \(sdk) metal \(files.map({ $0.string }).joined(separator: " ")) -o \(out.string)")
log.info("Archive: \(CFAbsoluteTimeGetCurrent() - start)")
defeatCache += 1
return out
}
private func topPaths(from paths: [Path]) -> [Path] {
var top: [(Path, [Path])] = []
paths.forEach { path in
// See if its already contained by the topDirectories
guard top.first(where: { (_, children) -> Bool in
return children.contains(path)
}) == nil else { return }
if path.isDirectory {
top.append((path, (try? path.recursiveChildren()) ?? []))
} else {
let dir = path.parent()
let children = (try? dir.recursiveChildren()) ?? []
if children.contains(path) {
top.append((dir, children))
} else {
top.append((path, []))
}
}
}
return top.map { $0.0 }
}
}
fileprivate func shell(_ command: String) -> String {
#if os(OSX)
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
#else
return "Not sure how to shell out in a Catalyst app"
#endif
}
import Foundation
public struct Paths {
public typealias Filter = ((Path) -> Bool)
public let include: [Path]
public let exclude: [Path]
public let allPaths: [Path]
private var filter: Filter? = nil
public var isEmpty: Bool {
return allPaths.isEmpty
}
public init(include: [Path], exclude: [Path] = []) {
self.include = include
self.exclude = exclude
let include = self.include.flatMap { $0.allPaths }
let exclude = self.exclude.flatMap { $0.allPaths }
self.allPaths = Array(Set(include).subtracting(Set(exclude))).sorted()
}
public func filter(_ filter: @escaping Filter) -> Self {
var result = self
result.filter = filter
return result
}
}
#if os(OSX)
public extension Paths {
typealias ProcessCallback = (Result<[Path], Error>) -> Void
typealias ProcessOnceCallBack = ([Path]) throws -> Void
typealias WatchCallBack = ([FolderWatcher.Event]) -> Void
func process(_ callback: @escaping ProcessCallback) -> [FolderWatcher.Local] {
func processAndHandleErrors() {
do {
try processOnce() {
callback(.success($0))
}
} catch {
callback(.failure(error))
}
}
processAndHandleErrors()
return watch() { _ in
processAndHandleErrors()
}
}
func watch(_ onEvent: @escaping WatchCallBack) -> [FolderWatcher.Local] {
let sourceWatchers = topPaths(from: allPaths).map { watchPath -> FolderWatcher.Local in
return FolderWatcher.Local(path: watchPath.string) { events in
var eventPaths = events
.filter { $0.flag.contains(.isFile) }
if let filter = self.filter {
eventPaths = eventPaths.filter { filter(Path($0.path)) }
}
if !eventPaths.isEmpty {
onEvent(eventPaths)
}
}
}
return sourceWatchers
}
func processOnce(_ callback: ProcessOnceCallBack) throws {
let startScan = CFAbsoluteTimeGetCurrent()
log.info("Scanning sources...")
var allResults: [Path] = []
let excludeSet = Set(exclude
.map { $0.isDirectory ? try? $0.recursiveChildren() : [$0] }
.compactMap({ $0 }).flatMap({ $0 }))
for from in include {
let fileList = from.isDirectory ? try from.recursiveChildren() : [from]
var sources = fileList
.filter { $0.exists }
.filter {
return !excludeSet.contains($0)
}
if let filter = self.filter {
sources = sources.filter { filter($0) }
}
allResults.append(contentsOf: sources)
}
log.info("Process all files: \(CFAbsoluteTimeGetCurrent() - startScan). \(allResults.count) files found.")
if !allResults.isEmpty {
try callback(allResults)
}
}
private func topPaths(from paths: [Path]) -> [Path] {
var top: [(Path, [Path])] = []
paths.forEach { path in
// See if its already contained by the topDirectories
guard top.first(where: { (_, children) -> Bool in
return children.contains(path)
}) == nil else { return }
if path.isDirectory {
top.append((path, (try? path.recursiveChildren()) ?? []))
} else {
let dir = path.parent()
let children = (try? dir.recursiveChildren()) ?? []
if children.contains(path) {
top.append((dir, children))
} else {
top.append((path, []))
}
}
}
return top.map { $0.0 }
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment