iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 14
0
Modern Web

謙虛,踏實的Web Assembly練習系列 第 14

[練習 13] 在Web Worker跑WebAssembly

  • 分享至 

  • xImage
  •  

透過Worker載入WebAssembly

概念很簡單,就是網頁端只把ImageData取出的資料,放進SharedArrayBuffer丟給Worker,Worker處理完之後寫回收到的SharedArrayBuffer,然後告訴網頁端已完成,網頁端就從SharedArrayBuffer把資料取出,寫回Canvas。

不過測試時發現一個問題,就是Worker中的WebAssembly物件,不支援compileStreaming()方法,所以必須調整一下,使用compile()方法:

wasm_util.js

(function(global) {
	global.Wasm = Wasm;
	function Wasm(_url) {
		let module = null;
		let url = _url;
		this.getModule = getModule;
		this.getInstance = getInstance;
		function getModule() {
			if(module === null) {
				return new Promise((resolve, reject) => {
					fetch(url)
					.then(response => response.arrayBuffer())
					.then(buf => {
						WebAssembly.compile(buf)
						.then(_module => {
							module = _module;
							resolve(_module)
						});
					})
					.catch(reason => reject(reason))
				});
			} else {
				return new Promise((resolve, reject) => resolve(module));
			}
		};
		function getInstance(importObjects) {
			if(module === null) {
				return new Promise((resolve, reject) => {
					getModule()
					.then(_module => {
						if(!!importObjects) resolve(new WebAssembly.Instance(_module, importObjects));
						else  resolve(new WebAssembly.Instance(_module));
					})
					.catch(reason => reject(reason))
				});
			} else {
				return new Promise((resolve, reject) => {
					if(!!importObjects) resolve(new WebAssembly.Instance(module, importObjects));
					else resolve(new WebAssembly.Instance(module));
				});
			}
		}
	}
})(this);

這樣,Worker透過importScripts()載入wasm_util.js後,就可以用之前一樣的方式載入WebAssembly程式。

調整轉灰階的WebAssembly程式

之前把原本影像跟灰階化影像寫入不同記憶體位置的方法,需要有兩倍的記憶體空間,而且還要分別計算兩邊記憶體的位置。但是其實可以把RGB三個位元的資料讀出並計算完之後,再把灰階依序覆蓋這三個位元。透明度的地方就不用改。

進一步簡化程式的方式,是記憶體位置不用每次加一,而是採用基底加上偏移的方式來做,這樣到迴圈結束時,只要把記憶體位置再加四就好。程式簡化一點,也比較不會出錯。

改完WebAssembly變短,變數也少了:

(module
	(memory (import "js" "buf") 1)
	(func (export "grey") (param $size i32)
		(local $ptr i32)
		(local $grey i32)
		(local $r i32)
		(local $g i32)
		(local $b i32)
		i32.const 0
		set_local $ptr

		(block $break
			(loop $while
				;; read R from memory[$ptr] and calculate
				get_local $ptr
				i32.load8_u
				i32.const 38
				i32.mul
				set_local $r

				;; read G from memory[$ptr+1] and calculate
				i32.const 1
				get_local $ptr
				i32.add
				i32.load8_u
				i32.const 75
				i32.mul
				set_local $g

				;; read B from memory[$ptr+2] and calculate
				i32.const 2
				get_local $ptr
				i32.add
				i32.load8_u
				i32.const 15
				i32.mul
				set_local $b

				;; calculate grey
				get_local $r
				get_local $g
				i32.add
				get_local $b
				i32.add
				i32.const 7
				i32.shr_u
				set_local $grey

				;; write R to memory[$ptr] 
				get_local $ptr
				get_local $grey
				i32.store8

				;; write G to memory[$ptr+1]
				i32.const 1
				get_local $ptr
				i32.add
				get_local $grey
				i32.store8

				;; write B to memory[$ptr+2]
				i32.const 2
				get_local $ptr
				i32.add
				get_local $grey
				i32.store8

				;; add $ptr by 4, skip opacity
				i32.const 4
				get_local $ptr
				i32.add
				tee_local $ptr
				;; if $ptr greater or equal to $base then break
				get_local $size
				i32.ge_u
				br_if $break
				;; next iteration
				br $while
			)
		)
	)
)

合體

然後就是網頁端跟Worker端的Javascript。先看一下Worker:

worker.js

importScripts('../wasm_util.js');

onmessage = function(e) {
	let page_required = Math.ceil(e.data.byteLength / (64 * 1024));
	let buf = new WebAssembly.Memory({initial: page_required});
	let view_p = new Uint8Array(e.data);
	let view_w = new Uint8Array(buf.buffer);
	for(let i=0; i<e.data.byteLength; i++) {
		view_w[i] = view_p[i];
	}
	let importObjects = {
		js: {
			buf: buf
		}
	};
	new Wasm('test015.wasm')
	.getInstance(importObjects)
	.then(instance => {
		instance.exports.grey(e.data.byteLength);
		let view_w = new Uint8Array(buf.buffer);
		for(let i=0; i<e.data.byteLength; i++) {
			view_p[i] = view_w[i];
		}
		postMessage('done');
	});
}

邏輯基本上跟之前程式差不多,只是多了從SharedArrayBuffer取出資料,寫回資料的動作。網頁端也是,透過SharedArrayBuffer把ImageData中的資料傳給Worker,處理完成再從SharedArrayBuffer中取回,然後寫入Canvas:

<html>
<body>
	<canvas id="canvas" width="640" height="480"></canvas><br>
	<button id="grayscale">Grayscale</button><br>
	<div id="panel" style="display:none"></div>
	<script>
	let canvas = document.getElementById('canvas').getContext('2d');
	let img = new Image();
	let size = 640 * 480 * 4;
	let image_data;
	let buf;
	img.src = 'IMG_1719_s.jpg';
	img.onload = function() {
		canvas.drawImage(img, 0, 0);
		console.log('image loaded');
	}
	document.getElementById('panel').appendChild(img);
	document.getElementById('grayscale').onclick = workergrayscale;
	let grayworker = new Worker('worker.js');
	grayworker.onmessage = function(e) {
		if(e.data === 'done') {
			let view = new Uint8Array(buf);
			for(let i=0; i<buf.byteLength; i++) {
				image_data.data[i] = view[i];
			}
			canvas.putImageData(image_data, 0, 0);
		}
	}
	function workergrayscale() {
		image_data = canvas.getImageData(0, 0, 640, 480);
		buf = new SharedArrayBuffer(image_data.data.length);
		let view = new Uint8Array(buf);
		for(let i=0; i<image_data.data.length; i++) {
			view[i] = image_data.data[i];
		}
		grayworker.postMessage(buf);
	}
	</script>
</body>
</html>

結果

都用同一張圖也太無趣,所以換了一個png檔。灰階化前:

Imgur

轉成灰階:

Imgur

完工...


恩,還是沒梗,回頭看了一下文件,發現之前都沒使用到global以及start這兩個區段,明天來試一試看看好了。


上一篇
[練習 12] 怎麼除錯
下一篇
[練習 14] global與start區段
系列文
謙虛,踏實的Web Assembly練習20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言