概念很簡單,就是網頁端只把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程式。
之前把原本影像跟灰階化影像寫入不同記憶體位置的方法,需要有兩倍的記憶體空間,而且還要分別計算兩邊記憶體的位置。但是其實可以把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檔。灰階化前:
轉成灰階:
完工...
恩,還是沒梗,回頭看了一下文件,發現之前都沒使用到global以及start這兩個區段,明天來試一試看看好了。