Skip to content

Commit 502da6e

Browse files
Refactor multi-threading example to be able to switch background/forground
1 parent a3a0a47 commit 502da6e

File tree

3 files changed

+107
-39
lines changed

3 files changed

+107
-39
lines changed

Examples/Multithreading/Sources/MyApp/Scene.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import ChibiRay
22

3-
func createDemoScene() -> Scene {
3+
func createDemoScene(size: Int) -> Scene {
44
return Scene(
5-
width: 800,
6-
height: 800,
5+
width: size,
6+
height: size,
77
fov: 90,
88
elements: [
99
.sphere(

Examples/Multithreading/Sources/MyApp/main.swift

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ struct Work: Sendable {
5757
}
5858
}
5959

60-
func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int, executor: WebWorkerTaskExecutor) async {
60+
func render(scene: Scene, ctx: JSObject, renderTimeElement: JSObject, concurrency: Int, executor: (some TaskExecutor)?) async {
6161

6262
let imageBuffer = UnsafeMutableBufferPointer<Color>.allocate(capacity: scene.width * scene.height)
6363
// Initialize the buffer with black color
@@ -67,12 +67,16 @@ func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int,
6767
let clock = ContinuousClock()
6868
let start = clock.now
6969

70+
func updateRenderTime() {
71+
let renderSceneDuration = clock.now - start
72+
renderTimeElement.textContent = .string("Render time: \(renderSceneDuration)")
73+
}
74+
7075
var checkTimer: JSValue?
7176
checkTimer = JSObject.global.setInterval!(JSClosure { _ in
7277
print("Checking thread work...")
7378
renderInCanvas(ctx: ctx, image: imageView)
74-
let renderSceneDuration = clock.now - start
75-
renderTime.textContent = .string("Render time: \(renderSceneDuration)")
79+
updateRenderTime()
7680
return .undefined
7781
}, 250)
7882

@@ -94,30 +98,41 @@ func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int,
9498
checkTimer = nil
9599

96100
renderInCanvas(ctx: ctx, image: imageView)
101+
updateRenderTime()
97102
imageBuffer.deallocate()
98103
print("All work done")
99104
}
100105

106+
func onClick() async throws {
107+
let document = JSObject.global.document
108+
109+
let canvasElement = document.getElementById("canvas").object!
110+
let renderTimeElement = document.getElementById("render-time").object!
111+
112+
let concurrency = max(Int(document.getElementById("concurrency").object!.value.string!) ?? 1, 1)
113+
let background = document.getElementById("background").object!.checked.boolean!
114+
let size = Int(document.getElementById("size").object!.value.string ?? "800")!
115+
116+
let ctx = canvasElement.getContext!("2d").object!
117+
118+
let scene = createDemoScene(size: size)
119+
let executor = background ? try await WebWorkerTaskExecutor(numberOfThreads: concurrency) : nil
120+
canvasElement.width = .number(Double(scene.width))
121+
canvasElement.height = .number(Double(scene.height))
122+
123+
await render(scene: scene, ctx: ctx, renderTimeElement: renderTimeElement, concurrency: concurrency, executor: executor)
124+
executor?.terminate()
125+
print("Render done")
126+
}
127+
101128
func main() async throws {
102-
let canvas = JSObject.global.document.getElementById("canvas").object!
103-
let renderButton = JSObject.global.document.getElementById("render-button").object!
104-
let concurrency = JSObject.global.document.getElementById("concurrency").object!
105-
concurrency.value = JSObject.global.navigator.hardwareConcurrency
106-
let scene = createDemoScene()
107-
canvas.width = .number(Double(scene.width))
108-
canvas.height = .number(Double(scene.height))
109-
110-
_ = renderButton.addEventListener!("click", JSClosure { _ in
129+
let renderButtonElement = JSObject.global.document.getElementById("render-button").object!
130+
let concurrencyElement = JSObject.global.document.getElementById("concurrency").object!
131+
concurrencyElement.value = JSObject.global.navigator.hardwareConcurrency
132+
133+
_ = renderButtonElement.addEventListener!("click", JSClosure { _ in
111134
Task {
112-
let canvas = JSObject.global.document.getElementById("canvas").object!
113-
let renderTime = JSObject.global.document.getElementById("render-time").object!
114-
let concurrency = JSObject.global.document.getElementById("concurrency").object!
115-
let concurrencyValue = max(Int(concurrency.value.string!) ?? 1, 1)
116-
let ctx = canvas.getContext!("2d").object!
117-
let executor = try await WebWorkerTaskExecutor(numberOfThreads: concurrencyValue)
118-
await render(scene: scene, ctx: ctx, renderTime: renderTime, concurrency: concurrencyValue, executor: executor)
119-
executor.terminate()
120-
print("Render done")
135+
try await onClick()
121136
}
122137
return JSValue.undefined
123138
})
@@ -126,3 +141,21 @@ func main() async throws {
126141
Task {
127142
try await main()
128143
}
144+
145+
146+
#if canImport(wasi_pthread)
147+
import wasi_pthread
148+
import WASILibc
149+
150+
/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
151+
/// the Swift concurrency runtime.
152+
@_cdecl("pthread_mutex_lock")
153+
func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
154+
// DO NOT BLOCK MAIN THREAD
155+
var ret: Int32
156+
repeat {
157+
ret = pthread_mutex_trylock(mutex)
158+
} while ret == EBUSY
159+
return ret
160+
}
161+
#endif

Examples/Multithreading/index.html

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,54 @@
11
<html>
2-
<head>
3-
<meta charset="utf-8">
4-
<title>Threading Example</title>
5-
</head>
6-
<body>
7-
<script type="module" src="Sources/JavaScript/index.js"></script>
8-
<h1>Threading Example</h1>
2+
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Threading Example</title>
6+
<style>
7+
.loading-emoji {
8+
font-size: 60px;
9+
width: 60px;
10+
height: 80px;
11+
}
12+
</style>
13+
<script>
14+
function rotateElement(element, speed = 2) {
15+
let degree = 0;
16+
setInterval(() => {
17+
degree = (degree + speed) % 360;
18+
element.style.transform = `rotate(${degree}deg)`;
19+
}, 20);
20+
}
21+
22+
document.addEventListener('DOMContentLoaded', () => {
23+
const loadingCircle = document.getElementById('loading-emoji');
24+
rotateElement(loadingCircle);
25+
});
26+
</script>
27+
</head>
28+
29+
<body>
30+
<script type="module" src="Sources/JavaScript/index.js"></script>
31+
<h1>Threading Example</h1>
32+
<p>
933
<div>
10-
<p>
11-
<label for="concurrency">Concurrency:</label>
12-
<input id="concurrency" type="number">
13-
<button id="render-button">Render</button>
14-
</p>
15-
<p id="render-time"></p>
34+
<label for="background">Background:</label>
35+
<input id="background" type="checkbox" checked>
1636
</div>
17-
<canvas id="canvas"></canvas>
18-
</body>
37+
<div>
38+
<label for="size">Size:</label>
39+
<input id="size" type="number" value="800">
40+
</div>
41+
<div>
42+
<label for="concurrency">Concurrency:</label>
43+
<input id="concurrency" type="number">
44+
<button id="render-button">Render</button>
45+
</div>
46+
</p>
47+
<p>
48+
<div id="loading-emoji" class="loading-emoji">🧵</div>
49+
</p>
50+
<p id="render-time"></p>
51+
<canvas id="canvas"></canvas>
52+
</body>
53+
1954
</html>

0 commit comments

Comments
 (0)