-
-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathmain.swift
178 lines (152 loc) · 5.35 KB
/
main.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import ChibiRay
import JavaScriptEventLoop
import JavaScriptKit
JavaScriptEventLoop.installGlobalExecutor()
WebWorkerTaskExecutor.installGlobalExecutor()
func renderInCanvas(ctx: JSObject, image: ImageView) {
let imageData = ctx.createImageData!(image.width, image.height).object!
let data = imageData.data.object!
for y in 0..<image.height {
for x in 0..<image.width {
let index = (y * image.width + x) * 4
let pixel = image[x, y]
data[index] = .number(Double(pixel.red * 255))
data[index + 1] = .number(Double(pixel.green * 255))
data[index + 2] = .number(Double(pixel.blue * 255))
data[index + 3] = .number(Double(255))
}
}
_ = ctx.putImageData!(imageData, 0, 0)
}
struct ImageView: @unchecked Sendable {
let width, height: Int
let buffer: UnsafeMutableBufferPointer<Color>
subscript(x: Int, y: Int) -> Color {
get {
return buffer[y * width + x]
}
nonmutating set {
buffer[y * width + x] = newValue
}
}
}
struct Work: Sendable {
let scene: Scene
let imageView: ImageView
let yRange: CountableRange<Int>
init(scene: Scene, imageView: ImageView, yRange: CountableRange<Int>) {
self.scene = scene
self.imageView = imageView
self.yRange = yRange
}
func run() {
for y in yRange {
for x in 0..<scene.width {
let ray = Ray.createPrime(x: x, y: y, scene: scene)
let color = castRay(scene: scene, ray: ray, depth: 0)
imageView[x, y] = color
}
}
}
}
func render(
scene: Scene,
ctx: JSObject,
renderTimeElement: JSObject,
concurrency: Int,
executor: (some TaskExecutor)?
) async {
let imageBuffer = UnsafeMutableBufferPointer<Color>.allocate(capacity: scene.width * scene.height)
// Initialize the buffer with black color
imageBuffer.initialize(repeating: .black)
let imageView = ImageView(width: scene.width, height: scene.height, buffer: imageBuffer)
let clock = ContinuousClock()
let start = clock.now
func updateRenderTime() {
let renderSceneDuration = clock.now - start
renderTimeElement.textContent = .string("Render time: \(renderSceneDuration)")
}
var checkTimer: JSValue?
checkTimer = JSObject.global.setInterval!(
JSClosure { _ in
print("Checking thread work...")
renderInCanvas(ctx: ctx, image: imageView)
updateRenderTime()
return .undefined
},
250
)
await withTaskGroup(of: Void.self) { group in
let yStride = scene.height / concurrency
for i in 0..<concurrency {
let yRange = i * yStride..<(i + 1) * yStride
let work = Work(scene: scene, imageView: imageView, yRange: yRange)
group.addTask(executorPreference: executor) { work.run() }
}
// Remaining rows
if scene.height % concurrency != 0 {
let work = Work(scene: scene, imageView: imageView, yRange: (concurrency * yStride)..<scene.height)
group.addTask(executorPreference: executor) { work.run() }
}
}
_ = JSObject.global.clearInterval!(checkTimer!)
checkTimer = nil
renderInCanvas(ctx: ctx, image: imageView)
updateRenderTime()
imageBuffer.deallocate()
print("All work done")
}
func onClick() async throws {
let document = JSObject.global.document
let canvasElement = document.getElementById("canvas").object!
let renderTimeElement = document.getElementById("render-time").object!
let concurrency = max(Int(document.getElementById("concurrency").object!.value.string!) ?? 1, 1)
let background = document.getElementById("background").object!.checked.boolean!
let size = Int(document.getElementById("size").object!.value.string ?? "800")!
let ctx = canvasElement.getContext!("2d").object!
let scene = createDemoScene(size: size)
let executor = background ? try await WebWorkerTaskExecutor(numberOfThreads: concurrency) : nil
canvasElement.width = .number(Double(scene.width))
canvasElement.height = .number(Double(scene.height))
await render(
scene: scene,
ctx: ctx,
renderTimeElement: renderTimeElement,
concurrency: concurrency,
executor: executor
)
executor?.terminate()
print("Render done")
}
func main() async throws {
let renderButtonElement = JSObject.global.document.getElementById("render-button").object!
let concurrencyElement = JSObject.global.document.getElementById("concurrency").object!
concurrencyElement.value = JSObject.global.navigator.hardwareConcurrency
_ = renderButtonElement.addEventListener!(
"click",
JSClosure { _ in
Task {
try await onClick()
}
return JSValue.undefined
}
)
}
Task {
try await main()
}
#if canImport(wasi_pthread)
import wasi_pthread
import WASILibc
/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
/// the Swift concurrency runtime.
@_cdecl("pthread_mutex_lock")
func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
// DO NOT BLOCK MAIN THREAD
var ret: Int32
repeat {
ret = pthread_mutex_trylock(mutex)
} while ret == EBUSY
return ret
}
#endif