-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.html
424 lines (354 loc) · 16.8 KB
/
index.html
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Plotting Fractals Using WebAssembly Threads and Web Workers</title>
<link rel="stylesheet" href="./assets/style.css"charset="utf8">
</head>
<body>
<table>
<tr><td><a href="https://red-badger.com" target="_new"><img class="rb_logo" src="./assets/red-badger.svg"/></a></td>
<td style="text-align: right;">
<a href="https://github.com/redbadger/raw_wasm" target="_new">Git Repository</a></td></tr>
<tr><td colspan="3">
<h1 class="heading">Plotting Fractals Using WebAssembly Threads and Web Workers</h1></td></tr>
<tr><td colspan="3">
<table>
<tr><td>Web Workers</td>
<td><input class="horizontal" id="workers" type="range"></td>
<td><span id="workers_txt"></span></td></tr>
<tr><td>Maximum iterations</td>
<td><input class="horizontal" id="max_iters" type="range"></td>
<td><span id="max_iters_txt"></span></td></tr>
<tr><td colspan="3"><h3>Left-click to zoom in, right-click to zoom out</h3></td></tr>
<tr><td>Zoom level</td>
<td><span id="ppu_txt"></span></td></tr>
</table></td></tr>
<tr><td colspan="3">Mandelbrot set rendered in <span id="mb_runtime"></span> ms</td></tr>
<tr><td colspan="2"><canvas id="mandelImage" style="border: 1px solid black"></canvas></td>
<td style="vertical-align: text-top;">
<table class="perf" border="0">
<tr><td class="narrow visible"id="w0_cell1">W0:</td><td><span id="w0"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w1_cell1">W1:</td><td id="w1_cell2"><span id="w1"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w2_cell1">W2:</td><td id="w2_cell2"><span id="w2"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w3_cell1">W3:</td><td id="w3_cell2"><span id="w3"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w4_cell1">W4:</td><td id="w4_cell2"><span id="w4"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w5_cell1">W5:</td><td id="w5_cell2"><span id="w5"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w6_cell1">W6:</td><td id="w6_cell2"><span id="w6"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w7_cell1">W7:</td><td id="w7_cell2"><span id="w7"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w8_cell1">W8:</td><td id="w8_cell2"><span id="w8"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w9_cell1">W9:</td><td id="w9_cell2"><span id="w9"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w10_cell1">W10:</td><td id="w10_cell2"><span id="w10"></span> ms</td></tr>
<tr><td class="narrow hidden" id="w11_cell1">W11:</td><td id="w11_cell2"><span id="w11"></span> ms</td></tr>
</table></td></tr>
<tr><td colspan="3">Mandelbrot Set coordinates (<span id="x_complex_coord"></span>, <span id="y_complex_coord"></span>)</td></tr>
<tr><td colspan="3">Julia Set calculated in <span id="julia_runtime"></span> ms</td></tr>
<tr><td colspan="2">
<canvas id="juliaImage" style="border: 1px solid black"></canvas></td></tr>
</table>
<script>
function $id(el) { return document.getElementById(el) }
const microPrecision = val => Math.round(val * 10000) / 10000
const interval = time => microPrecision(time.end - time.start)
const GREEN = '#00CC66'
const WASM_PAGE_SIZE = 1024 * 64
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Canvas properties
const CANVAS_WIDTH = 800
const CANVAS_HEIGHT = 450
const DEFAULT_X_ORIGIN = -0.5
const DEFAULT_Y_ORIGIN = 0
let X_ORIGIN = DEFAULT_X_ORIGIN
let Y_ORIGIN = DEFAULT_Y_ORIGIN
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Worker thread slider properties
const RANGE_WORKERS = { MIN : 1, MAX : 12, STEP : 1, DEFAULT : 4 }
let WORKERS = RANGE_WORKERS.DEFAULT
let worker_list = new Array()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Max iters slider parameters
const RANGE_MAX_ITERS = { MIN : 100, MAX : 32768, STEP : 100, DEFAULT : 1000 }
let MAX_ITERS = RANGE_MAX_ITERS.DEFAULT
// Define max/min zoom level limits (PPU = pixels per unit in the complex plane)
const MAX_PPU = 6553600 // Allow for 16 zoom steps (100 * 2^16)
const MIN_PPU = CANVAS_WIDTH / 4 // Start by showing entire Mandelbrot Set
let PPU = MIN_PPU
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Partial function to translate the mouse X/Y canvas position to the corresponding X/Y coordinates in the complex
// plane.
const canvas_pxl_to_coord = (cnvsDim, ppu, origin) => mousePos => origin + ((mousePos - (cnvsDim / 2)) / ppu)
let mandel_x_pos_to_coord = canvas_pxl_to_coord(CANVAS_WIDTH, PPU, X_ORIGIN)
let mandel_y_pos_to_coord = canvas_pxl_to_coord(CANVAS_HEIGHT, PPU, Y_ORIGIN)
let last_julia = {
x_coord : null,
y_coord : null
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Return a value clamped to the magnitude of the canvas image dimension accounting also for the canvas border width
const offset_to_clamped_pos = (offset, dim, offsetDim) => {
let pos = offset - ((offsetDim - dim) / 2)
return pos < 0 ? 0 : pos > dim ? dim : pos
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Initialise a slider UI element
const init_slider = (slider_id, range, current_val, evt_name, evt_fn) => {
let s = $id(slider_id)
s.max = range.MAX
s.min = range.MIN
s.step = range.STEP
s.value = current_val
s.addEventListener(evt_name, evt_fn, false)
$id(`${slider_id}_txt`).innerHTML = current_val
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Generate a worker message object
const gen_worker_msg = (p_action, p_worker_id, p_fractal, p_zx, p_zy, p_host_fns) =>
p_fractal === "mb"
? { action : p_action,
payload : {
host_fns : p_host_fns || {},
fractal : {
name : p_fractal,
width : CANVAS_WIDTH,
height : CANVAS_HEIGHT,
origin_x : X_ORIGIN,
origin_y : Y_ORIGIN,
zx : 0.0,
zy : 0.0,
ppu : PPU,
is_mandelbrot : true,
img_offset : mImageStart,
},
max_iters : MAX_ITERS,
worker_id : p_worker_id || 0,
},
}
: { action : p_action,
payload : {
host_fns : p_host_fns || {},
fractal : {
name : p_fractal,
width : CANVAS_WIDTH,
height : CANVAS_HEIGHT,
origin_x : 0.0,
origin_y : 0.0,
zx : p_zx,
zy : p_zy,
ppu : MIN_PPU,
is_mandelbrot : false,
img_offset : jImageStart,
},
max_iters : MAX_ITERS,
worker_id : p_worker_id || 0,
},
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// As long as a calculation is not currently running, send a message to every worker to start drawing a new fractal image
const draw_fractal = (p_name, p_zx, p_zy) => {
if (!plot_time.isActive) {
plot_time.wCount = 0
plot_time.isActive = true
plot_time.start = performance.now()
// Invoke all the workers
worker_list.map((w, idx) => {
$id(`w${idx}_cell1`).style.backgroundColor = GREEN
w.postMessage(gen_worker_msg('exec', idx, p_name, p_zx, p_zy))
})
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Partial function for handling image zoom in/out events
const zoom = zoom_in => evt => {
// Suppress default context menu when zooming out
if (!zoom_in) evt.preventDefault()
// Transform the mouse pointer pixel location to coordinates in the complex plane
X_ORIGIN = mandel_x_pos_to_coord(offset_to_clamped_pos(evt.offsetX, evt.target.width, evt.target.offsetWidth))
Y_ORIGIN = mandel_y_pos_to_coord(offset_to_clamped_pos(evt.offsetY, evt.target.height, evt.target.offsetHeight))
// Change zoom level
PPU = zoom_in
? (new_ppu => new_ppu > MAX_PPU ? MAX_PPU : new_ppu)(PPU * 2)
: (new_ppu => new_ppu < MIN_PPU ? MIN_PPU : new_ppu)(PPU / 2)
$id("ppu_txt").innerHTML = PPU
// If we're back out to the default zoom level, then reset the Mandelbrot Set image origin
if (PPU === MIN_PPU) {
X_ORIGIN = DEFAULT_X_ORIGIN
Y_ORIGIN = DEFAULT_Y_ORIGIN
}
// Update the mouse position helper functions using the new X/Y origin and zoom level
mandel_x_pos_to_coord = canvas_pxl_to_coord(CANVAS_WIDTH, PPU, X_ORIGIN)
mandel_y_pos_to_coord = canvas_pxl_to_coord(CANVAS_HEIGHT, PPU, Y_ORIGIN)
// Redraw the Mandelbrot Set
draw_fractal("mb")
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Mouse move event handler
const mouse_track = evt => {
// Transform the mouse pointer pixel location to coordinates in the complex plane
last_julia.x_coord = mandel_x_pos_to_coord(
offset_to_clamped_pos(evt.offsetX, evt.target.width, evt.target.offsetWidth)
)
// Flip sign because on a canvas, positive Y direction is down
last_julia.y_coord = mandel_y_pos_to_coord(
offset_to_clamped_pos(evt.offsetY, evt.target.height, evt.target.offsetHeight)
) * -1
// Display the mouse pointer's current position as coordinates in the complex plane
$id('x_complex_coord').innerHTML = last_julia.x_coord
$id('y_complex_coord').innerHTML = last_julia.y_coord
// Draw the new Julia Set
draw_fractal("julia", last_julia.x_coord, last_julia.y_coord)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Max iters slider event handler
const update_max_iters = evt => {
MAX_ITERS = evt.target.value
$id("max_iters_txt").innerHTML = MAX_ITERS
// Palette regeneration does not need to be delegated to a worker thread
wasm_colour.instance.exports.gen_palette(MAX_ITERS)
// Redraw Mandelbrot Set
draw_fractal("mb")
// Redraw last Julia Set
if (last_julia.x_coord !== null && last_julia.y_coord !== null)
draw_fractal("julia", last_julia.x_coord, last_julia.y_coord)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// For the fractal calculation to be distributed between worker threads, the index of the pixel being calculated must be
// accessible to each thread via shared memory. Each worker then accesses and modifies this value atomically.
// Offset 0 = i32 holding current Mandelbrot pixel index
// Offset 4 = i32 holding current Julia pixel index
const COUNTERS_LEN = 8
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Mandelbrot Set canvas
const mCanvas = $id('mandelImage')
mCanvas.width = CANVAS_WIDTH
mCanvas.height = CANVAS_HEIGHT
mCanvas.addEventListener('mousemove', mouse_track, false)
mCanvas.addEventListener('click', zoom(true), false)
mCanvas.addEventListener('contextmenu', zoom(false), false)
const mContext = mCanvas.getContext('2d')
const mImage = mContext.createImageData(mCanvas.width, mCanvas.height)
const mImagePages = Math.ceil(mImage.data.length / WASM_PAGE_SIZE)
const mImageStart = COUNTERS_LEN
const mImageEnd = mImageStart + mImage.data.length
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Julia Set canvas
const jCanvas = $id('juliaImage')
jCanvas.width = CANVAS_WIDTH
jCanvas.height = CANVAS_HEIGHT
const jContext = jCanvas.getContext('2d')
const jImage = jContext.createImageData(jCanvas.width, jCanvas.height)
const jImagePages = Math.ceil(jImage.data.length / WASM_PAGE_SIZE)
const jImageStart = WASM_PAGE_SIZE * mImagePages
const jImageEnd = jImageStart + jImage.data.length
const palettePages = 2
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Create WASM memory object for sharing resources from the host environment
const totalMemPages = mImagePages + jImagePages + palettePages
const wasmMemory = new WebAssembly.Memory({
initial : totalMemPages,
maximum : totalMemPages,
shared : true,
})
const wasmMem8 = new Uint8ClampedArray(wasmMemory.buffer)
const wasmMem32 = new Uint32Array(wasmMemory.buffer)
const host_fns = {
js : {
shared_mem : wasmMemory,
palette_offset : WASM_PAGE_SIZE * (mImagePages + jImagePages),
}
}
// Record worker thread activity
let plot_time = { start : 0, end : 0, wCount : 0, isActive : false }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Handle message received from worker thread
const worker_msg_handler =
({ data }) => {
const { action, payload } = data
const { worker_id, fractal, times } = payload
switch(action) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// One of the workers has finished
case 'exec_complete':
plot_time.wCount += 1
// Update this worker's performance time
$id(`w${worker_id}_cell1`).style.backgroundColor = 'white'
$id(`w${worker_id}`).innerHTML = interval(times.exec)
// Have all the workers finished yet?
if (plot_time.wCount === WORKERS) {
plot_time.end = performance.now()
$id(`${fractal}_runtime`).innerHTML = interval(plot_time)
switch(fractal) {
case "mb":
mImage.data.set(wasmMem8.slice(mImageStart, mImageEnd))
mContext.putImageData(mImage,0,0)
break
case "julia":
jImage.data.set(wasmMem8.slice(jImageStart, jImageEnd))
jContext.putImageData(jImage,0,0)
break
default:
}
// Reset X,Y iteration counters in shared memory
wasmMem32[0] = 0x00000000
wasmMem32[1] = 0x00000000
plot_time.wCount = 0
plot_time.isActive = false
}
default:
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Build required number of worker threads
const rebuild_workers = evt => {
if (evt) {
WORKERS = evt.target.valueAsNumber
$id("workers_txt").innerHTML = WORKERS
}
// If the number of workers has changed, terminate any existing workers then creating new ones
if (worker_list.length !== WORKERS) {
worker_list.map(w => w.terminate() )
worker_list.length = 0
plot_time.start = window.performance.now()
// Create new set of workers
for (let i=0; i<WORKERS; i++) {
// Worker 0 is always visible
if (i>0) {
$id(`w${i}_cell1`).classList.remove("hidden")
$id(`w${i}_cell2`).classList.remove("hidden")
$id(`w${i}_cell1`).classList.add("visible")
$id(`w${i}_cell2`).classList.add("visible")
}
let w = new Worker('./js/worker.js')
// Respond to messages received from the worker
w.onmessage = worker_msg_handler
// Initialise worker thread
$id(`w${i}_cell1`).style.backgroundColor = GREEN
w.postMessage(gen_worker_msg('init', i, 'mb', 0, 0, host_fns))
worker_list.push(w)
}
// Switch off unused worker performance fields in the UI
for (let i=WORKERS; i<RANGE_WORKERS.MAX; i++) {
$id(`w${i}_cell1`).classList.remove("visible")
$id(`w${i}_cell2`).classList.remove("visible")
$id(`w${i}_cell1`).classList.add("hidden")
$id(`w${i}_cell2`).classList.add("hidden")
}
}
}
let wasm_colour
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Async function to initialise the UI, create WASM colour palette module, generate colour palette then create the
// required number of Web Workers
const start = async () => {
// Initialise the UI
init_slider("max_iters", RANGE_MAX_ITERS, MAX_ITERS, "input", update_max_iters)
init_slider("workers", RANGE_WORKERS, WORKERS, "input", rebuild_workers)
$id("ppu_txt").innerHTML = PPU
// Palette generation does not need to be delegated to a worker thread
wasm_colour = await WebAssembly.instantiateStreaming(fetch("./wat/colour_palette.wasm"), host_fns)
wasm_colour.instance.exports.gen_palette(MAX_ITERS)
rebuild_workers()
}
start()
</script>
</body>
</html>