From 4541a78f01260a9c72e32f577d4508f416b84587 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Sun, 19 Jul 2026 00:41:42 +0000 Subject: [PATCH] Add Swift native AC runtime contract --- macos/runtime/.gitignore | 2 ++ macos/runtime/Package.swift | 12 ++++++++++++ macos/runtime/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ macos/runtime/Sources/ACNativeRuntime/PieceSupervisor.swift | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ macos/runtime/Sources/ACNativeRuntime/Runtime.swift | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ macos/runtime/Tests/ACNativeRuntimeTests/RuntimeTests.swift | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 file(s) changed, 327 insertion(s)(+), 0 deletion(s)(-) diff --git a/macos/runtime/.gitignore b/macos/runtime/.gitignore new file mode 100644 --- /dev/null +++ b/macos/runtime/.gitignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/macos/runtime/Package.swift b/macos/runtime/Package.swift new file mode 100644 --- /dev/null +++ b/macos/runtime/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "ACNativeRuntime", + platforms: [.macOS(.v14)], + products: [.library(name: "ACNativeRuntime", targets: ["ACNativeRuntime"])], + targets: [ + .target(name: "ACNativeRuntime"), + .testTarget(name: "ACNativeRuntimeTests", dependencies: ["ACNativeRuntime"]), + ] +) diff --git a/macos/runtime/README.md b/macos/runtime/README.md new file mode 100644 --- /dev/null +++ b/macos/runtime/README.md @@ -0,0 +1,42 @@ +# Aesthetic Computer Swift native runtime + +This package is the macOS analogue of `xbox/runtime`: an AC BIOS contract for a +native host with an embedded JavaScript interpreter. It does not require a +`WKWebView`, DOM, Safari, or Web Audio. A production host binds these protocols +to Metal, Core Audio (or a carefully preallocated `AVAudioEngine` graph), +GameController/AppKit, URLSession, and sandboxed storage. + +| AC piece surface | Swift | Xbox/C++ | +|---|---|---| +| `boot/sim/paint/act/leave` | `ACPiece` | `ac::xbox::Piece` | +| runtime context | `ACApi` | `ac::xbox::Api` | +| 2D draw commands | `ACGraphics` | `Graphics` | +| models/textures/shaders | `ACRenderer` (Metal handles) | future D3D renderer | +| immediate synth/sample | `ACSound` (Core Audio) | `Sound` (XAudio2) | +| normalized input | `ACInputMap` | `input_map.hpp` | +| downloaded source metadata | `ACPieceBundle` | `PieceBundle` | +| embedded JS seam | `ACJSEngine` | `JsEngine` | +| atomic live reload | `ACPieceSupervisor` actor | `PieceSupervisor` | + +The remote endpoint supplies UTF-8 source with an immutable version and SHA-256. +The supervisor enforces a source bound, verifies the digest, compiles and boots +in a fresh interpreter, then activates only at the host's frame boundary. The +old interpreter is retained during a bounded callback probation window. Throws +or watchdog overruns trigger rollback without restarting the native process. + +The JavaScript adapter should expose only AC bindings. Do not expose `Process`, +the filesystem, Objective-C reflection, raw sockets, browser globals, or dynamic +native loading. Remote content is piece data; native BIOS changes remain signed +application updates. + +Run the contract tests with: + +```sh +cd macos/runtime +swift test +``` + +`macos/fight-runner` remains the WebKit comparison harness. This package is the +foundation for the direct native runner: add concrete Metal/Core Audio adapters +and a pinned embedded interpreter, then run the same `fight` bundle used by the +Xbox BIOS. diff --git a/macos/runtime/Sources/ACNativeRuntime/PieceSupervisor.swift b/macos/runtime/Sources/ACNativeRuntime/PieceSupervisor.swift new file mode 100644 --- /dev/null +++ b/macos/runtime/Sources/ACNativeRuntime/PieceSupervisor.swift @@ -0,0 +1,79 @@ +import CryptoKit +import Foundation + +public struct ACPieceBundle: Sendable, Equatable { + public var slug: String; public var version: String; public var source: Data; public var sha256: String + public init(slug: String, version: String, source: Data, sha256: String) { + self.slug = slug; self.version = version; self.source = source; self.sha256 = sha256 + } +} +public struct ACJSLimits: Sendable, Equatable { + public var maxSourceBytes = 2 * 1024 * 1024; public var maxHeapBytes = 32 * 1024 * 1024 + public var maxCallbackMicroseconds: UInt64 = 8_000; public var probationCallbacks = 120 + public init() {} +} +public protocol ACJSPiece: ACPiece { var slug: String { get }; var version: String { get } } +public protocol ACJSEngine: AnyObject { + // JavaScriptCore/QuickJS adapters expose only ACApi bindings, never DOM or arbitrary native APIs. + func compile(_ bundle: ACPieceBundle, limits: ACJSLimits) throws -> any ACJSPiece +} +public protocol ACPieceBundleSource: AnyObject, Sendable { func fetch(slug: String) async throws -> ACPieceBundle } + +public enum ACRuntimeError: Error, Equatable { + case sourceTooLarge, digestMismatch, noStagedPiece, callbackBudgetExceeded, callbackFailed +} + +public enum ACDigest { + public static func sha256(_ data: Data) -> String { + SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + } +} + +// Main-actor isolation makes frame-boundary stage/activate/rollback atomic. +// Remote loaders hop to the render actor only after their network work finishes. +@MainActor public final class ACPieceSupervisor { + private let engine: ACJSEngine; private let limits: ACJSLimits + private var active: (any ACJSPiece)?; private var staged: (any ACJSPiece)?; private var fallback: (any ACJSPiece)? + private var probationRemaining = 0 + public private(set) var generation: UInt64 = 0 + public init(engine: ACJSEngine, limits: ACJSLimits = .init()) { self.engine = engine; self.limits = limits } + + public func fetchAndStage(slug: String, from source: ACPieceBundleSource, api: ACApi) async throws { + try stage(await source.fetch(slug: slug), api: api) + } + public func stage(_ bundle: ACPieceBundle, api: ACApi) throws { + guard bundle.source.count <= limits.maxSourceBytes else { throw ACRuntimeError.sourceTooLarge } + guard ACDigest.sha256(bundle.source) == bundle.sha256.lowercased() else { throw ACRuntimeError.digestMismatch } + let candidate = try engine.compile(bundle, limits: limits) + try candidate.boot(api) + staged = candidate + } + public func activate(api: ACApi) throws { + guard let candidate = staged else { throw ACRuntimeError.noStagedPiece } + if let current = active { current.leave(api); fallback = current } + active = candidate; staged = nil; probationRemaining = limits.probationCallbacks; generation &+= 1 + } + public func rollback(api: ACApi) -> Bool { + guard let previous = fallback else { return false } + active?.leave(api); active = previous; fallback = nil; probationRemaining = 0; generation &+= 1 + return true + } + public func sim(api: ACApi) throws { try invoke(api: api) { try $0.sim(api) }; api.simCount &+= 1 } + public func paint(api: ACApi) throws { try invoke(api: api) { try $0.paint(api) }; api.paintCount &+= 1 } + public func act(api: ACApi, event: ACEvent) throws { try invoke(api: api) { try $0.act(api, event: event) } } + + private func invoke(api: ACApi, _ callback: (any ACJSPiece) throws -> Void) throws { + guard let piece = active else { return } + let start = ContinuousClock.now + do { try callback(piece) } catch { + _ = rollback(api: api); throw ACRuntimeError.callbackFailed + } + let elapsed = start.duration(to: .now) + let microseconds = UInt64(max(0, elapsed.components.seconds * 1_000_000 + elapsed.components.attoseconds / 1_000_000_000_000)) + if microseconds > limits.maxCallbackMicroseconds { + _ = rollback(api: api); throw ACRuntimeError.callbackBudgetExceeded + } + if probationRemaining > 0 { probationRemaining -= 1 } + if probationRemaining == 0 { fallback = nil } + } +} diff --git a/macos/runtime/Sources/ACNativeRuntime/Runtime.swift b/macos/runtime/Sources/ACNativeRuntime/Runtime.swift new file mode 100644 --- /dev/null +++ b/macos/runtime/Sources/ACNativeRuntime/Runtime.swift @@ -0,0 +1,137 @@ +import Foundation + +public struct ACEvent: Sendable, Equatable { + public var name: String + public var value: Float + public var timestampMicroseconds: UInt64 + public init(_ name: String, value: Float = 1, timestampMicroseconds: UInt64 = 0) { + self.name = name; self.value = value; self.timestampMicroseconds = timestampMicroseconds + } +} + +public struct ACScreen: Sendable, Equatable { + public var width: Int; public var height: Int; public var scale: Float + public init(width: Int = 1920, height: Int = 1080, scale: Float = 1) { + self.width = width; self.height = height; self.scale = scale + } +} + +public struct ACClock: Sendable, Equatable { + public var monotonicMicroseconds: UInt64 = 0 + public var unixMilliseconds: Int64 = 0 + public var seconds: Double = 0 + public init() {} +} + +public struct ACSystem: Sendable, Equatable { + public var platform = "macos" + public var version = "" + public var handle = "" + public var dark = true + public var online = false + public init() {} +} + +public struct ACGamepadState: Sendable, Equatable { + public var leftX: Float = 0, leftY: Float = 0, rightX: Float = 0, rightY: Float = 0 + public var leftTrigger: Float = 0, rightTrigger: Float = 0 + public var down: Set = [] + public init() {} + public func pressed(_ button: String) -> Bool { down.contains(button) } +} + +public struct ACColor: Sendable, Equatable { + public var red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8 + public init(_ red: UInt8, _ green: UInt8, _ blue: UInt8, _ alpha: UInt8 = 255) { + self.red = red; self.green = green; self.blue = blue; self.alpha = alpha + } +} + +public struct ACTransform: Sendable, Equatable { + public var matrix: [Float] + public init(matrix: [Float] = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]) { self.matrix = matrix } +} + +public protocol ACGraphics: AnyObject { + func wipe(_ color: ACColor) + func box(x: Float, y: Float, width: Float, height: Float, color: ACColor) + func line(x1: Float, y1: Float, x2: Float, y2: Float, width: Float, color: ACColor) + func write(_ text: String, x: Float, y: Float, size: Float, color: ACColor) +} + +// Handles are opaque native resources owned by the Metal host. +public struct ACModel: Sendable, Hashable { public let id: UInt64; public init(id: UInt64) { self.id = id } } +public struct ACTexture: Sendable, Hashable { public let id: UInt64; public init(id: UInt64) { self.id = id } } +public struct ACShader: Sendable, Hashable { public let id: UInt64; public init(id: UInt64) { self.id = id } } + +public protocol ACRenderer: AnyObject { + func loadModel(bytes: Data, format: String) throws -> ACModel + func loadTexture(bytes: Data, format: String) throws -> ACTexture + func makeShader(source: String, entryPoint: String) throws -> ACShader + func draw(model: ACModel, transform: ACTransform, texture: ACTexture?, shader: ACShader?) +} + +public struct ACSynthVoice: Sendable, Equatable { + public var frequencyHz: Float = 440, durationSeconds: Float = 0.1, volume: Float = 0.25 + public var attackSeconds: Float = 0.001 + public var wave = "sine" + public init() {} +} + +public protocol ACSound: AnyObject { + // Implementations submit immediately to a preallocated Core Audio render path. + func synth(_ voice: ACSynthVoice) + func playImmediate(buffer: UInt64, volume: Float) + func stopAll() + var sampleRate: Int { get } +} + +public protocol ACNetwork: AnyObject { + func request(_ request: URLRequest) async throws -> (Data, URLResponse) +} +public protocol ACStorage: AnyObject { + func data(for key: String) throws -> Data? + func set(_ data: Data?, for key: String) throws +} +public struct ACTelemetry: Sendable, Equatable { + public var type: String; public var requestID: String; public var timestampMicroseconds: UInt64; public var json: String + public init(type: String, requestID: String = "", timestampMicroseconds: UInt64 = 0, json: String = "{}") { + self.type = type; self.requestID = requestID; self.timestampMicroseconds = timestampMicroseconds; self.json = json + } +} +public protocol ACTelemetrySink: AnyObject { func send(_ event: ACTelemetry) } + +public final class ACApi { + public var screen: ACScreen; public var clock = ACClock(); public var system = ACSystem() + public var gamepad = ACGamepadState(); public var simCount: UInt64 = 0; public var paintCount: UInt64 = 0 + public let graphics: ACGraphics; public let renderer: ACRenderer; public let sound: ACSound + public let network: ACNetwork; public let storage: ACStorage; public let telemetry: ACTelemetrySink + public init(screen: ACScreen = .init(), graphics: ACGraphics, renderer: ACRenderer, sound: ACSound, + network: ACNetwork, storage: ACStorage, telemetry: ACTelemetrySink) { + self.screen = screen; self.graphics = graphics; self.renderer = renderer; self.sound = sound + self.network = network; self.storage = storage; self.telemetry = telemetry + } +} + +public protocol ACPiece: AnyObject { + func boot(_ api: ACApi) throws + func sim(_ api: ACApi) throws + func paint(_ api: ACApi) throws + func act(_ api: ACApi, event: ACEvent) throws + func leave(_ api: ACApi) +} +public extension ACPiece { func boot(_ api: ACApi) throws {}; func leave(_ api: ACApi) {} } + +public enum ACInputMap { + // GameController and AppKit adapters emit these stable piece-level names. + public static let keyboard: [UInt16: String] = [ + 0x00: "a", 0x01: "s", 0x02: "d", 0x0D: "w", 0x7B: "ArrowLeft", 0x7C: "ArrowRight", + 0x7D: "ArrowDown", 0x7E: "ArrowUp", 0x24: "Enter", 0x31: "Space", 0x35: "Escape" + ] + public static let gameController: [String: String] = [ + "buttonA": "a", "buttonB": "b", "buttonX": "x", "buttonY": "y", + "leftShoulder": "lb", "rightShoulder": "rb", "leftTrigger": "lt", "rightTrigger": "rt", + "dpad.up": "ArrowUp", "dpad.down": "ArrowDown", "dpad.left": "ArrowLeft", "dpad.right": "ArrowRight", + "buttonMenu": "Enter" + ] +} diff --git a/macos/runtime/Tests/ACNativeRuntimeTests/RuntimeTests.swift b/macos/runtime/Tests/ACNativeRuntimeTests/RuntimeTests.swift new file mode 100644 --- /dev/null +++ b/macos/runtime/Tests/ACNativeRuntimeTests/RuntimeTests.swift @@ -0,0 +1,55 @@ +import Foundation +import Testing +@testable import ACNativeRuntime + +final class NullHost: ACGraphics, ACRenderer, ACSound, ACNetwork, ACStorage, ACTelemetrySink { + var sampleRate = 48_000 + func wipe(_ color: ACColor) {}; func box(x: Float,y: Float,width: Float,height: Float,color: ACColor) {} + func line(x1: Float,y1: Float,x2: Float,y2: Float,width: Float,color: ACColor) {} + func write(_ text: String,x: Float,y: Float,size: Float,color: ACColor) {} + func loadModel(bytes: Data, format: String) throws -> ACModel { .init(id: 1) } + func loadTexture(bytes: Data, format: String) throws -> ACTexture { .init(id: 1) } + func makeShader(source: String, entryPoint: String) throws -> ACShader { .init(id: 1) } + func draw(model: ACModel, transform: ACTransform, texture: ACTexture?, shader: ACShader?) {} + func synth(_ voice: ACSynthVoice) {}; func playImmediate(buffer: UInt64, volume: Float) {}; func stopAll() {} + func request(_ request: URLRequest) async throws -> (Data, URLResponse) { fatalError() } + func data(for key: String) throws -> Data? { nil }; func set(_ data: Data?, for key: String) throws {} + func send(_ event: ACTelemetry) {} +} +final class FakePiece: ACJSPiece { + let slug: String; let version: String; var fail = false; var sims = 0 + init(_ slug: String, _ version: String) { self.slug = slug; self.version = version } + func sim(_ api: ACApi) throws { sims += 1; if fail { throw ACRuntimeError.callbackFailed } } + func paint(_ api: ACApi) throws {}; func act(_ api: ACApi, event: ACEvent) throws {} +} +final class FakeEngine: ACJSEngine { + var pieces: [FakePiece] = [] + func compile(_ bundle: ACPieceBundle, limits: ACJSLimits) throws -> any ACJSPiece { + let piece = FakePiece(bundle.slug, bundle.version); pieces.append(piece); return piece + } +} +func bundle(_ slug: String) -> ACPieceBundle { + let data = Data("export function sim() {}".utf8) + return .init(slug: slug, version: "1", source: data, sha256: ACDigest.sha256(data)) +} + +@MainActor @Test func validatesDigestAndMapsInput() async throws { + let host = NullHost(), engine = FakeEngine(), supervisor = ACPieceSupervisor(engine: engine) + let api = ACApi(graphics: host, renderer: host, sound: host, network: host, storage: host, telemetry: host) + var bad = bundle("fight"); bad.sha256 = String(repeating: "0", count: 64) + #expect(throws: ACRuntimeError.digestMismatch) { try supervisor.stage(bad, api: api) } + #expect(ACInputMap.keyboard[0x7E] == "ArrowUp") + #expect(ACInputMap.gameController["buttonX"] == "x") +} + +@MainActor @Test func activatesAndRollsBackFailedCandidate() async throws { + let host = NullHost(), engine = FakeEngine(), supervisor = ACPieceSupervisor(engine: engine) + let api = ACApi(graphics: host, renderer: host, sound: host, network: host, storage: host, telemetry: host) + try supervisor.stage(bundle("stable"), api: api); try supervisor.activate(api: api) + try supervisor.stage(bundle("candidate"), api: api); try supervisor.activate(api: api) + engine.pieces.last!.fail = true + #expect(throws: ACRuntimeError.callbackFailed) { try supervisor.sim(api: api) } + #expect(supervisor.generation == 3) + try supervisor.sim(api: api) + #expect(engine.pieces.first!.sims == 1) +} -- tangled.sh