A comment is a last resort, and these were not one: section banners, doc headers naming what the declaration below already names, and notes saying in prose what the next line says in code. What is left is what the code cannot show -- an ordering that matters, a platform quirk, why the obvious way fails.
347 lines
12 KiB
Swift
347 lines
12 KiB
Swift
import Foundation
|
|
import Synchronization
|
|
|
|
/// One thing a scene draws: the author's own stages, over their own vertices,
|
|
/// with their own uniforms and where it sits. A scene holds as many as the
|
|
/// author cares to make, and each can be changed on its own.
|
|
public struct CanvasDrawable: Sendable, Equatable {
|
|
public var geometry: CanvasGeometry
|
|
public var vertexGLSL: String = ""
|
|
public var fragmentGLSL: String = ""
|
|
public var vertexMetal: String = ""
|
|
public var fragmentMetal: String = ""
|
|
public var transform: [Float] = CanvasDrawable.identity
|
|
/// What the author wrote, before the host wrapped it in declarations.
|
|
public var authorVertex: String = ""
|
|
public var authorFragment: String = ""
|
|
public var primitive: CanvasGeometry.Primitive = .triangles
|
|
/// Named by the author, in the order they declared them.
|
|
public var uniforms: [CanvasUniform] = []
|
|
public var drivers: [CanvasDriver] = []
|
|
public var buffers: [CanvasBuffer] = []
|
|
public var images: [CanvasImage] = []
|
|
public var isVisible = true
|
|
|
|
public static let identity: [Float] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
|
|
|
|
public init(geometry: CanvasGeometry = CanvasGeometry(attributes: [])) {
|
|
self.geometry = geometry
|
|
}
|
|
|
|
/// The uniforms as they stand at a reading of the clock, so a moving value
|
|
/// needs no word from the image to keep moving.
|
|
public func uniforms(at now: Double) -> [CanvasUniform] {
|
|
guard !drivers.isEmpty else { return uniforms }
|
|
|
|
return uniforms.map { uniform in
|
|
guard let driver = drivers.first(where: { $0.name == uniform.name }) else {
|
|
return uniform
|
|
}
|
|
return CanvasUniform(name: uniform.name, values: driver.value(at: now))
|
|
}
|
|
}
|
|
|
|
/// The author's uniforms laid out as the generated block declares them,
|
|
/// so Metal reads each where it expects to.
|
|
public func packedParams(at now: Double) -> [Float] {
|
|
var packed: [Float] = []
|
|
for uniform in uniforms(at: now) {
|
|
let padding = (uniform.alignment - packed.count % uniform.alignment) % uniform.alignment
|
|
packed.append(contentsOf: repeatElement(0, count: padding))
|
|
packed.append(contentsOf: uniform.values.prefix(uniform.components))
|
|
if uniform.values.count < uniform.components {
|
|
packed.append(contentsOf: repeatElement(
|
|
0, count: uniform.components - uniform.values.count))
|
|
}
|
|
}
|
|
// A block is rounded up to a whole vec4.
|
|
let tail = (4 - packed.count % 4) % 4
|
|
packed.append(contentsOf: repeatElement(0, count: tail))
|
|
return packed
|
|
}
|
|
}
|
|
|
|
/// A picture the shader samples across, rather than reads by index: an icon,
|
|
/// a screenshot, whatever the image drew. Pixels arrive as the image holds
|
|
/// them, one word each, which is the byte order both renderers upload.
|
|
public struct CanvasImage: Sendable, Equatable {
|
|
public let name: String
|
|
public var pixels: [UInt32]
|
|
public let width: Int
|
|
public let height: Int
|
|
/// Bumped whenever the pixels are handed over, so a renderer can tell a
|
|
/// fresh picture from the one it holds without reading a megabyte to
|
|
/// find out they match.
|
|
public let stamp: UInt64
|
|
|
|
public init(name: String, pixels: [UInt32], width: Int, height: Int, stamp: UInt64) {
|
|
self.name = name
|
|
self.pixels = pixels
|
|
self.width = width
|
|
self.height = height
|
|
self.stamp = stamp
|
|
}
|
|
}
|
|
|
|
/// A run of values too large for a uniform: a memory window, a waveform, a
|
|
/// point cloud's worth of samples. Carried as a texture the shader reads by
|
|
/// index, so scrubbing is a fresh window rather than a rebuilt scene.
|
|
public struct CanvasBuffer: Sendable, Equatable {
|
|
public let name: String
|
|
public var values: [Float]
|
|
/// As a picture's, and for the same reason.
|
|
public let stamp: UInt64
|
|
|
|
public init(name: String, values: [Float], stamp: UInt64) {
|
|
self.name = name
|
|
self.values = values
|
|
self.stamp = stamp
|
|
}
|
|
|
|
/// Texture side, chosen so the run fits a square-ish sheet the driver
|
|
/// will accept. A row is the unit an index divides by.
|
|
public var width: Int {
|
|
max(1, min(4096, Int(Double(values.count).squareRoot().rounded(.up))))
|
|
}
|
|
|
|
public var height: Int {
|
|
max(1, (values.count + width - 1) / width)
|
|
}
|
|
|
|
public func padded() -> [Float] {
|
|
var sheet = values
|
|
sheet.append(contentsOf: repeatElement(0, count: width * height - values.count))
|
|
return sheet
|
|
}
|
|
}
|
|
|
|
/// What a value should do over time, said once rather than driven frame by
|
|
/// frame. The renderers work it out on their own clock, so the image is
|
|
/// never in the frame path.
|
|
public struct CanvasDriver: Sendable, Equatable {
|
|
public enum Kind: Int32, Sendable, Equatable {
|
|
case ramp = 0
|
|
case oscillate = 1
|
|
}
|
|
|
|
public let name: String
|
|
public let kind: Kind
|
|
public let from: [Float]
|
|
public let to: [Float]
|
|
/// Seconds a ramp takes, or a full swing of an oscillation.
|
|
public let seconds: Float
|
|
/// When it set off, read off `now`. Stamped where the movement is asked
|
|
/// for rather than where it is first drawn: a frame that arrives late has
|
|
/// to draw where the value is by then, not start it over.
|
|
public let startedAt: Double
|
|
|
|
/// Monotonic seconds. Whoever sets a driver and whoever draws it both read
|
|
/// the clock here, so the two agree without either carrying a timestamp.
|
|
public static var now: Double {
|
|
ProcessInfo.processInfo.systemUptime
|
|
}
|
|
|
|
public init(name: String, kind: Kind, from: [Float], to: [Float], seconds: Float, startedAt: Double = CanvasDriver.now) {
|
|
self.name = name
|
|
self.kind = kind
|
|
self.from = from
|
|
self.to = to
|
|
self.seconds = seconds
|
|
self.startedAt = startedAt
|
|
}
|
|
|
|
public func value(at now: Double) -> [Float] {
|
|
let elapsed = Float(now - startedAt)
|
|
let fraction: Float
|
|
switch kind {
|
|
case .ramp:
|
|
fraction = seconds <= 0 ? 1 : min(max(elapsed / seconds, 0), 1)
|
|
case .oscillate:
|
|
let period = seconds <= 0 ? 1 : seconds
|
|
fraction = 0.5 - 0.5 * cos(2 * .pi * elapsed / period)
|
|
}
|
|
return zip(from, to).map { $0 + ($1 - $0) * fraction }
|
|
}
|
|
}
|
|
|
|
/// A value the author named, of whatever width they asked for. Declaring it
|
|
/// is what lets the host write the declaration and pack the buffer to match,
|
|
/// so a scene is not confined to a vocabulary chosen here.
|
|
public struct CanvasUniform: Sendable, Equatable {
|
|
public let name: String
|
|
public var values: [Float]
|
|
|
|
public init(name: String, values: [Float]) {
|
|
self.name = name
|
|
self.values = values
|
|
}
|
|
|
|
var components: Int { min(max(values.count, 1), 16) }
|
|
|
|
/// Floats it occupies, and what it must start on, by std140's rules.
|
|
var alignment: Int {
|
|
switch components {
|
|
case 1: return 1
|
|
case 2: return 2
|
|
default: return 4
|
|
}
|
|
}
|
|
|
|
var glslType: String {
|
|
switch components {
|
|
case 1: return "float"
|
|
case 2: return "vec2"
|
|
case 3: return "vec3"
|
|
case 16: return "mat4"
|
|
default: return "vec4"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What a canvas window shows. Held by handle so the image can keep hold of
|
|
/// one, change it, and have the drawing follow.
|
|
public struct CanvasScene: Sendable, Equatable {
|
|
public var drawables: [Int: CanvasDrawable] = [:]
|
|
public var order: [Int] = []
|
|
|
|
public init() {}
|
|
|
|
public var ordered: [CanvasDrawable] {
|
|
order.compactMap { drawables[$0] }.filter(\.isVisible)
|
|
}
|
|
}
|
|
|
|
/// The scenes the image is holding. Handles rather than one staged canvas, so
|
|
/// a snippet can build several and change any of them.
|
|
///
|
|
/// Reached from the image's own thread, which is why the state sits behind a
|
|
/// lock rather than on the main actor: a handle has to come back at once.
|
|
public final class CanvasRegistry: Sendable {
|
|
public static let shared = CanvasRegistry()
|
|
|
|
private let state = Mutex(State())
|
|
|
|
private struct State {
|
|
var scenes: [Int: CanvasScene] = [:]
|
|
var inputs: [Int: CanvasInput] = [:]
|
|
var scales: [Int: Float] = [:]
|
|
var pending: Set<Int> = []
|
|
var nextHandle = 1
|
|
var nextStamp: UInt64 = 1
|
|
}
|
|
|
|
/// Told when a scene changes, so whatever shows it can follow. Called on
|
|
/// the main thread.
|
|
@MainActor public static var onChange: ((Int, CanvasScene) -> Void)?
|
|
|
|
public func makeScene() -> Int {
|
|
state.withLock { state in
|
|
let handle = state.nextHandle
|
|
state.nextHandle += 1
|
|
state.scenes[handle] = CanvasScene()
|
|
return handle
|
|
}
|
|
}
|
|
|
|
public func nextStamp() -> UInt64 {
|
|
state.withLock { state in
|
|
state.nextStamp += 1
|
|
return state.nextStamp
|
|
}
|
|
}
|
|
|
|
/// How many the image is holding, which is how a leak shows.
|
|
public var sceneCount: Int {
|
|
state.withLock { $0.scenes.count }
|
|
}
|
|
|
|
public func scene(_ handle: Int) -> CanvasScene? {
|
|
state.withLock { $0.scenes[handle] }
|
|
}
|
|
|
|
public func discard(_ handle: Int) {
|
|
state.withLock { $0.scenes[handle] = nil }
|
|
}
|
|
|
|
public func makeDrawable(in handle: Int) -> Int {
|
|
state.withLock { state in
|
|
guard var scene = state.scenes[handle] else { return 0 }
|
|
|
|
let drawable = state.nextHandle
|
|
state.nextHandle += 1
|
|
scene.drawables[drawable] = CanvasDrawable()
|
|
scene.order.append(drawable)
|
|
state.scenes[handle] = scene
|
|
return drawable
|
|
}
|
|
}
|
|
|
|
/// Changes one drawable. Answers the scene as it now stands, so the caller
|
|
/// can hand it to whatever is showing it.
|
|
@discardableResult
|
|
public func update(
|
|
_ drawable: Int,
|
|
in handle: Int,
|
|
_ change: (inout CanvasDrawable) -> Void
|
|
) -> CanvasScene? {
|
|
state.withLock { state in
|
|
guard var scene = state.scenes[handle], var subject = scene.drawables[drawable] else {
|
|
return nil
|
|
}
|
|
|
|
change(&subject)
|
|
scene.drawables[drawable] = subject
|
|
state.scenes[handle] = scene
|
|
return scene
|
|
}
|
|
}
|
|
|
|
public func remove(_ drawable: Int, from handle: Int) -> CanvasScene? {
|
|
state.withLock { state in
|
|
guard var scene = state.scenes[handle] else { return nil }
|
|
|
|
scene.drawables[drawable] = nil
|
|
scene.order.removeAll { $0 == drawable }
|
|
state.scenes[handle] = scene
|
|
return scene
|
|
}
|
|
}
|
|
|
|
/// What the pointer and keyboard are doing over the scene, as the view
|
|
/// showing it last reported.
|
|
public func input(_ handle: Int) -> CanvasInput {
|
|
state.withLock { $0.inputs[handle] ?? CanvasInput() }
|
|
}
|
|
|
|
public func reportInput(_ handle: Int, _ change: (inout CanvasInput) -> Void) {
|
|
state.withLock { change(&$0.inputs[handle, default: CanvasInput()]) }
|
|
}
|
|
|
|
/// Physical pixels to a logical one, as the view showing the scene last
|
|
/// reported. Nothing has drawn it yet until one does.
|
|
public func scale(_ handle: Int) -> Float {
|
|
state.withLock { $0.scales[handle] ?? 0 }
|
|
}
|
|
|
|
public func reportScale(_ handle: Int, _ scale: Float) {
|
|
state.withLock { $0.scales[handle] = scale }
|
|
}
|
|
|
|
/// Hands the scene to whoever shows it, on the thread they expect. A
|
|
/// snippet setting four uniforms in a row is one change to draw, so only
|
|
/// the first schedules and what arrives is the scene as it ends up.
|
|
public func publish(_ handle: Int) {
|
|
let waiting = state.withLock { !$0.pending.insert(handle).inserted }
|
|
guard !waiting else { return }
|
|
|
|
DispatchQueue.main.async {
|
|
let latest = self.state.withLock { state -> CanvasScene? in
|
|
state.pending.remove(handle)
|
|
return state.scenes[handle]
|
|
}
|
|
guard let latest else { return }
|
|
MainActor.assumeIsolated { CanvasRegistry.onChange?(handle, latest) }
|
|
}
|
|
}
|
|
}
|