@swiftbrowser/native 0.18.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/bin/swiftbrowser-native.mjs +47 -0
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +66 -8
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +24 -0
- package/dist/encoding.js.map +1 -1
- package/dist/fetch.d.ts +27 -13
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +117 -45
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +24 -6
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +108 -15
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/ios.d.ts +79 -0
- package/dist/ios.d.ts.map +1 -0
- package/dist/ios.js +181 -0
- package/dist/ios.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -7
- package/dist/runtime.js.map +1 -1
- package/dist/subtle.d.ts +1 -1
- package/dist/subtle.d.ts.map +1 -1
- package/dist/subtle.js +9 -5
- package/dist/subtle.js.map +1 -1
- package/dist/webSocket.d.ts +12 -5
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +37 -11
- package/dist/webSocket.js.map +1 -1
- package/package.json +7 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +56 -8
- package/src/encoding.ts +22 -0
- package/src/fetch.ts +111 -48
- package/src/hostRequests.ts +129 -22
- package/src/index.ts +1 -2
- package/src/ios.ts +253 -0
- package/src/runtime.ts +23 -8
- package/src/subtle.ts +10 -6
- package/src/webSocket.ts +37 -13
- package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
- package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
- package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SwiftBrowserShader
|
|
2
|
+
|
|
3
|
+
The native side of [@swiftbrowser/shader](../../../packages/shader): a
|
|
4
|
+
shader written in the shader language, compiled by the package's TypeScript
|
|
5
|
+
compiler to the Metal Shading Language (beside GLSL and JavaScript for a
|
|
6
|
+
page), drawn by an `MTKView` over a `ShaderView`'s box. The JavaScript only
|
|
7
|
+
sends the shader, its uniforms and its textures; nothing per pixel runs
|
|
8
|
+
there.
|
|
9
|
+
|
|
10
|
+
It is a package of its own, as SwiftBrowserDSP is, so only an app that
|
|
11
|
+
draws with shaders links it. @swiftbrowser/native ships it beside the core
|
|
12
|
+
(`node_modules/@swiftbrowser/native/swift/Packages/SwiftBrowserShader`; it
|
|
13
|
+
depends on `SwiftBrowserNative` at `../..`, here and there). Link it into
|
|
14
|
+
the app's Xcode project with `npx swiftbrowser native add shader` (or
|
|
15
|
+
`native init --with shader`) and install it at launch:
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
import SwiftBrowserShader
|
|
19
|
+
|
|
20
|
+
init() {
|
|
21
|
+
SwiftBrowserShader.install()
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That registers `ShaderView`'s view (`SwiftBrowserShader.view`) with
|
|
26
|
+
SwiftBrowserNative's `ViewRegistry`. Without it, a `ShaderView` shows its
|
|
27
|
+
name, as any registered view nothing registered does.
|
|
28
|
+
|
|
29
|
+
- **SwiftBrowserShaderCore** is the view's props alone: the shader's layout
|
|
30
|
+
(`ShaderLayout`, every uniform inside the floats a draw reads), its
|
|
31
|
+
textures, and the floats packed as the JavaScript packs them on a page
|
|
32
|
+
(`packedUniforms`). No Apple framework, so it builds and tests on Linux.
|
|
33
|
+
- **SwiftBrowserShader** is the view: `ShaderRenderer` compiles a shader's
|
|
34
|
+
Metal when a view first draws it (`makeLibrary(source:)`, kept for every
|
|
35
|
+
view that draws the same source), uploads its textures as `r32Float`, and
|
|
36
|
+
draws one triangle over the view with the uniforms in a fragment buffer.
|
|
37
|
+
The `MTKView` draws when its props or its size change, or every display
|
|
38
|
+
frame when the view is `animated`. A shader its device does not compile
|
|
39
|
+
draws nothing and sends the compiler's message to `onError`, once.
|
|
40
|
+
|
|
41
|
+
The tests (`swift test`) read @swiftbrowser/shader's render fixtures
|
|
42
|
+
(packages/shader/test/fixtures/render.json) and, on a Mac, compile each
|
|
43
|
+
case's Metal, draw it into a texture and hold its pixels to the ones the
|
|
44
|
+
package's JavaScript backend drew (printed as `[shader render]`), as the
|
|
45
|
+
page's tests hold WebGL2's.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#if canImport(Metal)
|
|
2
|
+
import Metal
|
|
3
|
+
@_exported import SwiftBrowserShaderCore
|
|
4
|
+
|
|
5
|
+
/// What draws a shader with Metal: its pipeline (the library compiled once
|
|
6
|
+
/// for a source, kept for every view that draws it), its textures, and a
|
|
7
|
+
/// draw into a render pass. `ShaderView`'s `MTKView` draws through it, and
|
|
8
|
+
/// the tests draw into a texture of their own.
|
|
9
|
+
@MainActor
|
|
10
|
+
public final class ShaderRenderer {
|
|
11
|
+
public let device: MTLDevice
|
|
12
|
+
public let pixelFormat: MTLPixelFormat
|
|
13
|
+
private var pipeline: MTLRenderPipelineState?
|
|
14
|
+
private var source = ""
|
|
15
|
+
private var textures: [MTLTexture] = []
|
|
16
|
+
private var uploaded: [ShaderTextureData?] = []
|
|
17
|
+
/// A texture of one zero for a slot the view was given nothing for: a draw reads every slot it declares.
|
|
18
|
+
private lazy var blank: MTLTexture? = Self.texture(device: device, width: 1, height: 1, data: [0])
|
|
19
|
+
|
|
20
|
+
/// Pipelines by source and pixel format, for every view: a library compiles in tens of milliseconds.
|
|
21
|
+
private static var pipelines: [String: MTLRenderPipelineState] = [:]
|
|
22
|
+
|
|
23
|
+
public init(device: MTLDevice, pixelFormat: MTLPixelFormat = .bgra8Unorm) {
|
|
24
|
+
self.device = device
|
|
25
|
+
self.pixelFormat = pixelFormat
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public enum Failure: Error, CustomStringConvertible {
|
|
29
|
+
case compile(String)
|
|
30
|
+
case missing(String)
|
|
31
|
+
|
|
32
|
+
public var description: String {
|
|
33
|
+
switch self {
|
|
34
|
+
case .compile(let message): return message
|
|
35
|
+
case .missing(let name): return "the library has no function \(name)"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Makes ready to draw `props`: its pipeline compiled (or found) and its textures uploaded when they changed.
|
|
41
|
+
/// Throws the compiler's message when its Metal does not compile.
|
|
42
|
+
public func prepare(_ props: ShaderProps) throws {
|
|
43
|
+
guard let shader = props.shader else {
|
|
44
|
+
pipeline = nil
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if shader.msl != source {
|
|
48
|
+
source = shader.msl
|
|
49
|
+
pipeline = nil
|
|
50
|
+
pipeline = try Self.pipeline(device: device, source: shader.msl, pixelFormat: pixelFormat)
|
|
51
|
+
}
|
|
52
|
+
if props.textures != uploaded || textures.count != shader.textures.count {
|
|
53
|
+
uploaded = props.textures
|
|
54
|
+
textures = shader.textures.indices.compactMap { slot in
|
|
55
|
+
let data = slot < props.textures.count ? props.textures[slot] : nil
|
|
56
|
+
return data.flatMap { Self.texture(device: device, width: $0.width, height: $0.height, data: $0.data) } ?? blank
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Draws into `encoder`'s pass (which `prepare` made ready), the view `width` × `height` points at `scale` pixels
|
|
62
|
+
/// a point, `time` seconds in.
|
|
63
|
+
public func draw(_ props: ShaderProps, into encoder: MTLRenderCommandEncoder, width: Double, height: Double, scale: Double, time: Double) {
|
|
64
|
+
guard let pipeline, let shader = props.shader, textures.count == shader.textures.count else { return }
|
|
65
|
+
var floats = props.packedUniforms(width: width, height: height, scale: scale, time: time)
|
|
66
|
+
encoder.setRenderPipelineState(pipeline)
|
|
67
|
+
encoder.setFragmentBytes(&floats, length: MemoryLayout<Float>.stride * floats.count, index: 0)
|
|
68
|
+
for (slot, texture) in textures.enumerated() { encoder.setFragmentTexture(texture, index: slot) }
|
|
69
|
+
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static func pipeline(device: MTLDevice, source: String, pixelFormat: MTLPixelFormat) throws -> MTLRenderPipelineState {
|
|
73
|
+
let key = "\(pixelFormat.rawValue)\n\(source)"
|
|
74
|
+
if let found = pipelines[key] { return found }
|
|
75
|
+
let library: MTLLibrary
|
|
76
|
+
do {
|
|
77
|
+
library = try device.makeLibrary(source: source, options: nil)
|
|
78
|
+
} catch {
|
|
79
|
+
throw Failure.compile(String(describing: error))
|
|
80
|
+
}
|
|
81
|
+
guard let vertex = library.makeFunction(name: "sb_vertex") else { throw Failure.missing("sb_vertex") }
|
|
82
|
+
guard let fragment = library.makeFunction(name: "sb_fragment") else { throw Failure.missing("sb_fragment") }
|
|
83
|
+
let descriptor = MTLRenderPipelineDescriptor()
|
|
84
|
+
descriptor.vertexFunction = vertex
|
|
85
|
+
descriptor.fragmentFunction = fragment
|
|
86
|
+
descriptor.colorAttachments[0].pixelFormat = pixelFormat
|
|
87
|
+
let made = try device.makeRenderPipelineState(descriptor: descriptor)
|
|
88
|
+
pipelines[key] = made
|
|
89
|
+
return made
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A texture of one float a texel, read with `read` (no filtering: the shader's `sample` interpolates).
|
|
93
|
+
static func texture(device: MTLDevice, width: Int, height: Int, data: [Float]) -> MTLTexture? {
|
|
94
|
+
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r32Float, width: width, height: height, mipmapped: false)
|
|
95
|
+
descriptor.usage = .shaderRead
|
|
96
|
+
guard let texture = device.makeTexture(descriptor: descriptor), data.count >= width * height else { return nil }
|
|
97
|
+
data.withUnsafeBytes { bytes in
|
|
98
|
+
texture.replace(region: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: width * MemoryLayout<Float>.stride)
|
|
99
|
+
}
|
|
100
|
+
return texture
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
#endif
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#if canImport(MetalKit) && canImport(SwiftUI)
|
|
2
|
+
import MetalKit
|
|
3
|
+
import SwiftBrowserNative
|
|
4
|
+
import SwiftBrowserProtocol
|
|
5
|
+
import SwiftUI
|
|
6
|
+
|
|
7
|
+
/// The native side of @swiftbrowser/shader: installs `ShaderView`'s view, an
|
|
8
|
+
/// `MTKView` that draws the shader's Metal over its box.
|
|
9
|
+
///
|
|
10
|
+
/// ```swift
|
|
11
|
+
/// import SwiftBrowserShader
|
|
12
|
+
///
|
|
13
|
+
/// init() {
|
|
14
|
+
/// SwiftBrowserShader.install()
|
|
15
|
+
/// }
|
|
16
|
+
/// ```
|
|
17
|
+
@MainActor
|
|
18
|
+
public enum SwiftBrowserShader {
|
|
19
|
+
/// The name `ShaderView` is registered under, on both platforms (view.ts `SHADER_VIEW`).
|
|
20
|
+
public static let viewName = "SwiftBrowserShader.view"
|
|
21
|
+
|
|
22
|
+
/// Registers `ShaderView`'s SwiftUI view; call at launch, before the bundle loads.
|
|
23
|
+
public static func install() {
|
|
24
|
+
ViewRegistry.register(viewName) { context in
|
|
25
|
+
ShaderSurface(props: ShaderProps(props: context.props), send: context.send)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// `ShaderView` (@swiftbrowser/shader): the shader drawn when its props or its size change, or every display frame
|
|
31
|
+
/// when `animated`. A shader its device does not compile draws nothing and says why (`onError`), once.
|
|
32
|
+
struct ShaderSurface: View {
|
|
33
|
+
let props: ShaderProps
|
|
34
|
+
let send: (JSONValue) -> Void
|
|
35
|
+
|
|
36
|
+
var body: some View {
|
|
37
|
+
if let device = SharedDevice.device {
|
|
38
|
+
MetalShaderView(device: device, props: props, send: send)
|
|
39
|
+
} else {
|
|
40
|
+
Color.clear
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@MainActor
|
|
46
|
+
private enum SharedDevice {
|
|
47
|
+
static let device: MTLDevice? = MTLCreateSystemDefaultDevice()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Draws for an `MTKView`: on demand (`setNeedsDisplay`) unless animated.
|
|
51
|
+
@MainActor
|
|
52
|
+
final class ShaderDrawing: NSObject, MTKViewDelegate {
|
|
53
|
+
let renderer: ShaderRenderer
|
|
54
|
+
let queue: MTLCommandQueue?
|
|
55
|
+
var props: ShaderProps
|
|
56
|
+
var send: (JSONValue) -> Void
|
|
57
|
+
private var reported = ""
|
|
58
|
+
private var started = Date()
|
|
59
|
+
|
|
60
|
+
init(device: MTLDevice, props: ShaderProps, send: @escaping (JSONValue) -> Void) {
|
|
61
|
+
renderer = ShaderRenderer(device: device)
|
|
62
|
+
queue = device.makeCommandQueue()
|
|
63
|
+
self.props = props
|
|
64
|
+
self.send = send
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func configure(_ view: MTKView) {
|
|
68
|
+
view.device = renderer.device
|
|
69
|
+
view.colorPixelFormat = renderer.pixelFormat
|
|
70
|
+
view.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
|
|
71
|
+
view.framebufferOnly = true
|
|
72
|
+
view.delegate = self
|
|
73
|
+
#if os(macOS)
|
|
74
|
+
view.layer?.isOpaque = false
|
|
75
|
+
#else
|
|
76
|
+
view.isOpaque = false
|
|
77
|
+
view.backgroundColor = .clear
|
|
78
|
+
#endif
|
|
79
|
+
update(view)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// New props: drawn at once, or from now on every frame when animated.
|
|
83
|
+
func update(_ view: MTKView) {
|
|
84
|
+
if !props.animated { started = Date() }
|
|
85
|
+
view.isPaused = !props.animated
|
|
86
|
+
view.enableSetNeedsDisplay = !props.animated
|
|
87
|
+
view.preferredFramesPerSecond = 60
|
|
88
|
+
redraw(view)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func redraw(_ view: MTKView) {
|
|
92
|
+
#if os(macOS)
|
|
93
|
+
view.needsDisplay = true
|
|
94
|
+
#else
|
|
95
|
+
view.setNeedsDisplay()
|
|
96
|
+
#endif
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
|
100
|
+
redraw(view)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func draw(in view: MTKView) {
|
|
104
|
+
do {
|
|
105
|
+
try renderer.prepare(props)
|
|
106
|
+
} catch {
|
|
107
|
+
let message = String(describing: error)
|
|
108
|
+
if message != reported {
|
|
109
|
+
reported = message
|
|
110
|
+
send(.object(["error": .string(message)]))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let bounds = view.bounds.size
|
|
114
|
+
guard bounds.width > 0, bounds.height > 0, let pass = view.currentRenderPassDescriptor, let drawable = view.currentDrawable,
|
|
115
|
+
let buffer = queue?.makeCommandBuffer(), let encoder = buffer.makeRenderCommandEncoder(descriptor: pass) else { return }
|
|
116
|
+
// The scale the drawable has, so that a point's rows and its pixels line up.
|
|
117
|
+
let scale = view.drawableSize.height / bounds.height
|
|
118
|
+
renderer.draw(props, into: encoder, width: bounds.width, height: bounds.height, scale: scale, time: Date().timeIntervalSince(started))
|
|
119
|
+
encoder.endEncoding()
|
|
120
|
+
buffer.present(drawable)
|
|
121
|
+
buffer.commit()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#if os(macOS)
|
|
126
|
+
private struct MetalShaderView: NSViewRepresentable {
|
|
127
|
+
let device: MTLDevice
|
|
128
|
+
let props: ShaderProps
|
|
129
|
+
let send: (JSONValue) -> Void
|
|
130
|
+
|
|
131
|
+
func makeCoordinator() -> ShaderDrawing { ShaderDrawing(device: device, props: props, send: send) }
|
|
132
|
+
|
|
133
|
+
func makeNSView(context: Context) -> MTKView {
|
|
134
|
+
let view = MTKView(frame: .zero, device: device)
|
|
135
|
+
context.coordinator.configure(view)
|
|
136
|
+
return view
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func updateNSView(_ view: MTKView, context: Context) {
|
|
140
|
+
context.coordinator.send = send
|
|
141
|
+
guard context.coordinator.props != props else { return }
|
|
142
|
+
context.coordinator.props = props
|
|
143
|
+
context.coordinator.update(view)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
#else
|
|
147
|
+
private struct MetalShaderView: UIViewRepresentable {
|
|
148
|
+
let device: MTLDevice
|
|
149
|
+
let props: ShaderProps
|
|
150
|
+
let send: (JSONValue) -> Void
|
|
151
|
+
|
|
152
|
+
func makeCoordinator() -> ShaderDrawing { ShaderDrawing(device: device, props: props, send: send) }
|
|
153
|
+
|
|
154
|
+
func makeUIView(context: Context) -> MTKView {
|
|
155
|
+
let view = MTKView(frame: .zero, device: device)
|
|
156
|
+
context.coordinator.configure(view)
|
|
157
|
+
return view
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func updateUIView(_ view: MTKView, context: Context) {
|
|
161
|
+
context.coordinator.send = send
|
|
162
|
+
guard context.coordinator.props != props else { return }
|
|
163
|
+
context.coordinator.props = props
|
|
164
|
+
context.coordinator.update(view)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
#endif
|
|
168
|
+
#endif
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import SwiftBrowserProtocol
|
|
2
|
+
|
|
3
|
+
/// A compiled shader as `ShaderView` sends it natively (@swiftbrowser/shader's
|
|
4
|
+
/// `CompiledShader` with only its Metal): where each uniform's floats are,
|
|
5
|
+
/// how many floats a draw reads, its textures' names in slot order, and the
|
|
6
|
+
/// Metal library (`sb_vertex`, `sb_fragment`).
|
|
7
|
+
public struct ShaderLayout: Equatable, Sendable {
|
|
8
|
+
public struct Uniform: Equatable, Sendable {
|
|
9
|
+
public let name: String
|
|
10
|
+
public let offset: Int
|
|
11
|
+
public let size: Int
|
|
12
|
+
public let value: [Float]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public let uniforms: [Uniform]
|
|
16
|
+
public let floats: Int
|
|
17
|
+
public let textures: [String]
|
|
18
|
+
public let msl: String
|
|
19
|
+
|
|
20
|
+
/// The floats every shader starts with (shader.ts `BUILTIN_UNIFORMS`): width, height, scale, time.
|
|
21
|
+
public static let builtins = 4
|
|
22
|
+
|
|
23
|
+
public init?(json: JSONValue) {
|
|
24
|
+
guard let msl = json["msl"].string, let floats = json["floats"].int, floats >= Self.builtins, floats <= 1024 else { return nil }
|
|
25
|
+
var uniforms: [Uniform] = []
|
|
26
|
+
for entry in json["uniforms"].array ?? [] {
|
|
27
|
+
guard let name = entry["name"].string, let offset = entry["offset"].int, let size = entry["size"].int,
|
|
28
|
+
(1...4).contains(size), offset >= Self.builtins, offset + size <= floats else { return nil }
|
|
29
|
+
let value = (entry["value"].array ?? []).map { Float($0.double ?? 0) }
|
|
30
|
+
uniforms.append(Uniform(name: name, offset: offset, size: size, value: value))
|
|
31
|
+
}
|
|
32
|
+
self.uniforms = uniforms
|
|
33
|
+
self.floats = floats
|
|
34
|
+
self.textures = (json["textures"].array ?? []).compactMap(\.string)
|
|
35
|
+
self.msl = msl
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A float texture: `width` × `height` values, row by row (row 0 at the top).
|
|
40
|
+
public struct ShaderTextureData: Equatable, Sendable {
|
|
41
|
+
public let width: Int
|
|
42
|
+
public let height: Int
|
|
43
|
+
public let data: [Float]
|
|
44
|
+
|
|
45
|
+
public init?(json: JSONValue) {
|
|
46
|
+
guard let width = json["width"].int, let height = json["height"].int, width > 0, height > 0,
|
|
47
|
+
let values = json["data"].array, values.count >= width * height else { return nil }
|
|
48
|
+
self.width = width
|
|
49
|
+
self.height = height
|
|
50
|
+
self.data = values.prefix(width * height).map { Float($0.double ?? 0) }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// `ShaderView`'s props (packages/shader/src/view.ts): the shader, its uniforms by name, its textures by slot, and
|
|
55
|
+
/// whether it draws every frame.
|
|
56
|
+
public struct ShaderProps: Equatable, Sendable {
|
|
57
|
+
public let shader: ShaderLayout?
|
|
58
|
+
public let uniforms: JSONObject
|
|
59
|
+
public let textures: [ShaderTextureData?]
|
|
60
|
+
public let animated: Bool
|
|
61
|
+
|
|
62
|
+
public init(props: JSONValue) {
|
|
63
|
+
shader = ShaderLayout(json: props["shader"])
|
|
64
|
+
uniforms = props["uniforms"].object ?? [:]
|
|
65
|
+
textures = (props["textures"].array ?? []).map(ShaderTextureData.init(json:))
|
|
66
|
+
animated = props["animated"].bool ?? false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The floats a draw reads (shader.ts `packUniforms`): the view's size in points, its pixels a point and the
|
|
71
|
+
* seconds it has run, then each uniform as given (a number, a vector, or a `#rrggbb` color), its default when
|
|
72
|
+
* absent or of another size, a color of three components taking alpha 1 where four are wanted.
|
|
73
|
+
*/
|
|
74
|
+
public func packedUniforms(width: Double, height: Double, scale: Double, time: Double) -> [Float] {
|
|
75
|
+
guard let shader else { return [] }
|
|
76
|
+
var floats = [Float](repeating: 0, count: shader.floats)
|
|
77
|
+
floats[0] = Float(width)
|
|
78
|
+
floats[1] = Float(height)
|
|
79
|
+
floats[2] = Float(scale)
|
|
80
|
+
floats[3] = Float(time)
|
|
81
|
+
for uniform in shader.uniforms {
|
|
82
|
+
var given = uniforms[uniform.name].flatMap(Self.floats(of:))
|
|
83
|
+
if let three = given, three.count == 3, uniform.size == 4 { given = three + [1] }
|
|
84
|
+
let value = given.flatMap { $0.count == uniform.size ? $0 : nil } ?? uniform.value
|
|
85
|
+
for i in 0..<uniform.size { floats[uniform.offset + i] = i < value.count ? value[i] : 0 }
|
|
86
|
+
}
|
|
87
|
+
return floats
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// A uniform's value as floats: a number, an array of numbers, or a color.
|
|
91
|
+
static func floats(of value: JSONValue) -> [Float]? {
|
|
92
|
+
switch value {
|
|
93
|
+
case .number(let number): return number.isFinite ? [Float(number)] : nil
|
|
94
|
+
case .string(let text): return parseColor(text)
|
|
95
|
+
case .array(let items):
|
|
96
|
+
let numbers = items.compactMap(\.double)
|
|
97
|
+
return numbers.count == items.count ? numbers.map { Float($0) } : nil
|
|
98
|
+
default: return nil
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// A `#rrggbb` or `#rrggbbaa` color as 0 … 1 components (shader.ts `parseColor`).
|
|
104
|
+
public func parseColor(_ text: String) -> [Float]? {
|
|
105
|
+
var digits = Substring(text)
|
|
106
|
+
while let first = digits.first, first.isWhitespace { digits = digits.dropFirst() }
|
|
107
|
+
while let last = digits.last, last.isWhitespace { digits = digits.dropLast() }
|
|
108
|
+
guard digits.first == "#" else { return nil }
|
|
109
|
+
digits = digits.dropFirst()
|
|
110
|
+
guard digits.count == 6 || digits.count == 8 else { return nil }
|
|
111
|
+
var components: [Float] = []
|
|
112
|
+
var index = digits.startIndex
|
|
113
|
+
while index < digits.endIndex {
|
|
114
|
+
let next = digits.index(index, offsetBy: 2)
|
|
115
|
+
guard let byte = UInt8(digits[index..<next], radix: 16) else { return nil }
|
|
116
|
+
components.append(Float(byte) / 255)
|
|
117
|
+
index = next
|
|
118
|
+
}
|
|
119
|
+
return components
|
|
120
|
+
}
|