admin管理员组文章数量:1384214
In our WebGL application I'm trying to load and decode texture images in a web worker in order to avoid rendering hick-ups in the main thread. Using createImageBitmap in the worker and transferring the image bitmap back to the main thread works well, but in Chrome this will use three or more (maybe depending on number of cores?) separate workers (ThreadPoolForegroundWorker) which together with the main thread and my own worker will result in five threads.
I'm guessing this causes my remaining rendering disturbances on my quad core since I can see some inexplicable long times in the Performance feature of Chrome's DevTools.
So, can I limit the number of workers used by createImageBitmap somehow? Even if I transfer the images as blobs or array buffers to the main thread and activate createImageBitmap from there, its workers will pete with my own worker and the main thread.
I have tried creating regular Images in the worker instead to explicitly decode them there, but Image is not defined in the worker context, neither is document if I'd like to create them as elements. And regular Images are not transferable either, so creating them on the main thread and transferring them to the worker doesn't seem feasible either.
Looking forward to any suggestions...
In our WebGL application I'm trying to load and decode texture images in a web worker in order to avoid rendering hick-ups in the main thread. Using createImageBitmap in the worker and transferring the image bitmap back to the main thread works well, but in Chrome this will use three or more (maybe depending on number of cores?) separate workers (ThreadPoolForegroundWorker) which together with the main thread and my own worker will result in five threads.
I'm guessing this causes my remaining rendering disturbances on my quad core since I can see some inexplicable long times in the Performance feature of Chrome's DevTools.
So, can I limit the number of workers used by createImageBitmap somehow? Even if I transfer the images as blobs or array buffers to the main thread and activate createImageBitmap from there, its workers will pete with my own worker and the main thread.
I have tried creating regular Images in the worker instead to explicitly decode them there, but Image is not defined in the worker context, neither is document if I'd like to create them as elements. And regular Images are not transferable either, so creating them on the main thread and transferring them to the worker doesn't seem feasible either.
Looking forward to any suggestions...
Share Improve this question asked Nov 14, 2019 at 12:06 Andreas EkstrandAndreas Ekstrand 331 silver badge3 bronze badges 4- If decoding images is super slow, maybe you can consider using a pressed texture format like S3TC, so you don't have to do any png/jpeg decoding. This would have other performance benefits too. This implies large downloads, so you'd probably want to enable gzip on top of that. – prideout Commented Nov 14, 2019 at 18:27
- Thanks prideout, I have considered pression before and might get to that eventually. And thanks @gman for the elaborate reply below, I will go through it thoroughly the next few days. I realize calling createImageBitmap in a worker is really not useful, just moved it into the worker together with the loading for now. But for a possible quick solution before deIving into gman's suggestions, I would like to know if there's a way to control the number of workers used by the browser for the decoding if I call createImageBitmap from the main thread? – Andreas Ekstrand Commented Nov 15, 2019 at 6:39
- s3tc is not nearly as small as jpg for downloads so it really spends on your images. Plus s3tc is only available on desktop so if you ever cared about mobile you're in for a world of hurt with all the pressed formats out there. – user128511 Commented Nov 15, 2019 at 6:49
- Thanks, didn't know about the lack of support of s3tc on mobile, so I will stick with jpg for a while then. Still curious about the control of number of image decoding threads in the browser though - you seem knowledgeable in this domain, do you know anything about this as well? – Andreas Ekstrand Commented Nov 15, 2019 at 7:09
1 Answer
Reset to default 6There's no reason to use createImageBitmap in a worker (well, see bottom). The browser already decodes the image in a separate thread. Doing it in a worker doesn't get you anything. The bigger issue is there's no way for ImageBitmap to know how you are going to use the image when you finally pass it to WebGL. If you ask for a format that's different than what ImageBitmap decoded then WebGL has to convert and/or decode it again and you can't give ImageBitmap enough info to tell it the format you want it to decode in.
On top of that WebGL in Chrome has to transfer the data of the image from the render process to the GPU process which for a large image is a relatively big copy (1024x1024 by RGBA is 4meg)
A better API IMO would have allowed you to tell ImageBitmap what format you want and where you want it (CPU, GPU). That way the browser could prep the image asynchonously and have it require no heavy work when done.
In any case, here's a test. If you uncheck "update texture" then it's still downloading and decoding textures but it's just not calling gl.texImage2D
to upload the texture. In that case I see no jank (not proof that's the issue but that's where I think it is)
const m4 = twgl.m4;
const gl = document.querySelector('#webgl').getContext('webgl');
const ctx = document.querySelector('#graph').getContext('2d');
let update = true;
document.querySelector('#update').addEventListener('change', function() {
update = this.checked;
});
const vs = `
attribute vec4 position;
uniform mat4 matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = matrix * position;
v_texcoord = position.xy;
}
`
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const posLoc = gl.getAttribLocation(program, 'position');
const matLoc = gl.getUniformLocation(program, 'matrix');
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const m = m4.identity();
let frameCount = 0;
let previousTime = 0;
let imgNdx = 0;
let imgAspect = 1;
const imageUrls = [
'https://i.imgur./KjUybBD.png',
'https://i.imgur./AyOufBk.jpg',
'https://i.imgur./UKBsvV0.jpg',
'https://i.imgur./TSiyiJv.jpg',
];
async function loadNextImage() {
const url = `${imageUrls[imgNdx]}?cachebust=${performance.now()}`;
imgNdx = (imgNdx + 1) % imageUrls.length;
const res = await fetch(url, {mode: 'cors'});
const blob = await res.blob();
const bitmap = await createImageBitmap(blob, {
premultiplyAlpha: 'none',
colorSpaceConversion: 'none',
});
if (update) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
imgAspect = bitmap.width / bitmap.height;
}
setTimeout(loadNextImage, 1000);
}
loadNextImage();
function render(currentTime) {
const deltaTime = currentTime - previousTime;
previousTime = currentTime;
{
const {width, height} = ctx.canvas;
const x = frameCount % width;
const y = 1000 / deltaTime / 60 * height / 2;
ctx.fillStyle = frameCount % (width * 2) < width ? 'red' : 'blue';
ctx.clearRect(x, 0, 1, height);
ctx.fillRect(x, y, 1, height);
ctx.clearRect(0, 0, 30, 15);
ctx.fillText((1000 / deltaTime).toFixed(1), 2, 10);
}
gl.useProgram(program);
const dispAspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
m4.scaling([1 / dispAspect, 1, 1], m);
m4.rotateZ(m, currentTime * 0.001, m);
m4.scale(m, [imgAspect, 1, 1], m);
m4.translate(m, [-0.5, -0.5, 0], m);
gl.uniformMatrix4fv(matLoc, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 6);
++frameCount;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; margin: 2px; }
#ui { position: absolute; }
<script src="https://twgljs/dist/4.x/twgl-full.min.js"></script>
<div id="ui"><input type="checkbox" id="update" checked><label for="update">Update Texture</label></div>
<canvas id="webgl"></canvas>
<canvas id="graph"></canvas>
I'm pretty sure the only way you could maybe guarentee no jank is to decode the images yourself in a worker, transfer to the main thread as an arraybuffer, and upload to WebGL a few rows a frame with gl.bufferSubData
.
const m4 = twgl.m4;
const gl = document.querySelector('#webgl').getContext('webgl');
const ctx = document.querySelector('#graph').getContext('2d');
const vs = `
attribute vec4 position;
uniform mat4 matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = matrix * position;
v_texcoord = position.xy;
}
`
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const posLoc = gl.getAttribLocation(program, 'position');
const matLoc = gl.getUniformLocation(program, 'matrix');
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
function createTexture(gl) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return tex;
}
let drawingTex = createTexture(gl);
let loadingTex = createTexture(gl);
const m = m4.identity();
let frameCount = 0;
let previousTime = 0;
const workerScript = `
const ctx = new OffscreenCanvas(1, 1).getContext('2d');
let imgNdx = 0;
let imgAspect = 1;
const imageUrls = [
'https://i.imgur./KjUybBD.png',
'https://i.imgur./AyOufBk.jpg',
'https://i.imgur./UKBsvV0.jpg',
'https://i.imgur./TSiyiJv.jpg',
];
async function loadNextImage() {
const url = \`\${imageUrls[imgNdx]}?cachebust=\${performance.now()}\`;
imgNdx = (imgNdx + 1) % imageUrls.length;
const res = await fetch(url, {mode: 'cors'});
const blob = await res.blob();
const bitmap = await createImageBitmap(blob, {
premultiplyAlpha: 'none',
colorSpaceConversion: 'none',
});
ctx.canvas.width = bitmap.width;
ctx.canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = new Uint8Array(imgData.data);
postMessage({
width: imgData.width,
height: imgData.height,
data: data.buffer,
}, [data.buffer]);
}
onmessage = loadNextImage;
`;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const worker = new Worker(URL.createObjectURL(blob));
let imgAspect = 1;
worker.onmessage = async(e) => {
const {width, height, data} = e.data;
gl.bindTexture(gl.TEXTURE_2D, loadingTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
const maxRows = 20;
for (let y = 0; y < height; y += maxRows) {
const rows = Math.min(maxRows, height - y);
gl.bindTexture(gl.TEXTURE_2D, loadingTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, y, width, rows, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(data, y * width * 4, rows * width * 4));
await waitRAF();
}
const temp = loadingTex;
loadingTex = drawingTex;
drawingTex = temp;
imgAspect = width / height;
await waitMS(1000);
worker.postMessage('');
};
worker.postMessage('');
function waitRAF() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
function waitMS(ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function render(currentTime) {
const deltaTime = currentTime - previousTime;
previousTime = currentTime;
{
const {width, height} = ctx.canvas;
const x = frameCount % width;
const y = 1000 / deltaTime / 60 * height / 2;
ctx.fillStyle = frameCount % (width * 2) < width ? 'red' : 'blue';
ctx.clearRect(x, 0, 1, height);
ctx.fillRect(x, y, 1, height);
ctx.clearRect(0, 0, 30, 15);
ctx.fillText((1000 / deltaTime).toFixed(1), 2, 10);
}
gl.useProgram(program);
const dispAspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
m4.scaling([1 / dispAspect, 1, 1], m);
m4.rotateZ(m, currentTime * 0.001, m);
m4.scale(m, [imgAspect, 1, 1], m);
m4.translate(m, [-0.5, -0.5, 0], m);
gl.bindTexture(gl.TEXTURE_2D, drawingTex);
gl.uniformMatrix4fv(matLoc, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 6);
++frameCount;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; margin: 2px; }
<script src="https://twgljs/dist/4.x/twgl-full.min.js"></script>
<canvas id="webgl"></canvas>
<canvas id="graph"></canvas>
Note: I don't know that this will work either. Several places that are scary and browser implementation defined
What's the performance issues of resizing a canvas. The code is resizing the OffscreenCanvas in the worker. That could be a heavy operation with GPU reprocussions.
What's the performance of drawing an bitmap into a canvas? Again, big GPU perf issues as the browser has to transfer the image to the GPU in order to draw it into a GPU 2D canvas.
What's the performance get getImageData? Yet again the browser has to potentially freeze the GPU to read GPU memory to get the image data out.
There's a possible perf hit reszing the texture.
Only Chrome currently supports OffscreenCanvas
1, 2, 3, and 5 could all be solved by decoding jpg, png the image yourself though it really sucks the browser has the code to decode the image it's just you can't access the decoding code in any useful way.
For 4, If it's an issue it could be solved, by allocating the largest image size texture and then copying smaller textures into a rectangular area. Assuming that's an issue
const m4 = twgl.m4;
const gl = document.querySelector('#webgl').getContext('webgl');
const ctx = document.querySelector('#graph').getContext('2d');
const vs = `
attribute vec4 position;
uniform mat4 matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = matrix * position;
v_texcoord = position.xy;
}
`
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const posLoc = gl.getAttribLocation(program, 'position');
const matLoc = gl.getUniformLocation(program, 'matrix');
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
function createTexture(gl) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return tex;
}
let drawingTex = createTexture(gl);
let loadingTex = createTexture(gl);
const m = m4.identity();
let frameCount = 0;
let previousTime = 0;
const workerScript = `
importScripts(
// from https://github./eugeneware/jpeg-js
'https://greggman.github.io/doodles/js/JPG-decoder.js',
// from https://github./photopea/UPNG.js
'https://greggman.github.io/doodles/js/UPNG.js',
);
let imgNdx = 0;
let imgAspect = 1;
const imageUrls = [
'https://i.imgur./KjUybBD.png',
'https://i.imgur./AyOufBk.jpg',
'https://i.imgur./UKBsvV0.jpg',
'https://i.imgur./TSiyiJv.jpg',
];
function decodePNG(arraybuffer) {
return UPNG.decode(arraybuffer)
}
function decodeJPG(arrayBuffer) {
return decode(new Uint8Array(arrayBuffer), true);
}
const decoders = {
'image/png': decodePNG,
'image/jpeg': decodeJPG,
'image/jpg': decodeJPG,
};
async function loadNextImage() {
const url = \`\${imageUrls[imgNdx]}?cachebust=\${performance.now()}\`;
imgNdx = (imgNdx + 1) % imageUrls.length;
const res = await fetch(url, {mode: 'cors'});
const arrayBuffer = await res.arrayBuffer();
const type = res.headers.get('Content-Type');
let decoder = decoders[type];
if (!decoder) {
console.error('unknown image type:', type);
}
const imgData = decoder(arrayBuffer);
postMessage({
width: imgData.width,
height: imgData.height,
arrayBuffer: imgData.data.buffer,
}, [imgData.data.buffer]);
}
onmessage = loadNextImage;
`;
const blob = new Blob([workerScript], {type: 'application/javascript'});
const worker = new Worker(URL.createObjectURL(blob));
let imgAspect = 1;
worker.onmessage = async(e) => {
const {width, height, arrayBuffer} = e.data;
gl.bindTexture(gl.TEXTURE_2D, loadingTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
const maxRows = 20;
for (let y = 0; y < height; y += maxRows) {
const rows = Math.min(maxRows, height - y);
gl.bindTexture(gl.TEXTURE_2D, loadingTex);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, y, width, rows, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(arrayBuffer, y * width * 4, rows * width * 4));
await waitRAF();
}
const temp = loadingTex;
loadingTex = drawingTex;
drawingTex = temp;
imgAspect = width / height;
await waitMS(1000);
worker.postMessage('');
};
worker.postMessage('');
function waitRAF() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
function waitMS(ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function render(currentTime) {
const deltaTime = currentTime - previousTime;
previousTime = currentTime;
{
const {width, height} = ctx.canvas;
const x = frameCount % width;
const y = 1000 / deltaTime / 60 * height / 2;
ctx.fillStyle = frameCount % (width * 2) < width ? 'red' : 'blue';
ctx.clearRect(x, 0, 1, height);
ctx.fillRect(x, y, 1, height);
ctx.clearRect(0, 0, 30, 15);
ctx.fillText((1000 / deltaTime).toFixed(1), 2, 10);
}
gl.useProgram(program);
const dispAspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
m4.scaling([1 / dispAspect, 1, 1], m);
m4.rotateZ(m, currentTime * 0.001, m);
m4.scale(m, [imgAspect, 1, 1], m);
m4.translate(m, [-0.5, -0.5, 0], m);
gl.bindTexture(gl.TEXTURE_2D, drawingTex);
gl.uniformMatrix4fv(matLoc, false, m);
gl.drawArrays(gl.TRIANGLES, 0, 6);
++frameCount;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; margin: 2px; }
<script src="https://twgljs/dist/4.x/twgl-full.min.js"></script>
<canvas id="webgl"></canvas>
<canvas id="graph"></canvas>
note the jpeg decoder is slow. If you find or make a faster one please post a ment
Update
I just want to say that ImageBitmap
should be fast enough and that some of my ments above about it not having enough info might not be exactly right.
My current understanding is the entire point if ImageBitmap
was to make uploads fast. It's supposed to work by you give it a blob and asynchronously it loads that image into the GPU. When you call texImage2D
with it, the browser can "blit" (render with the GPU) that image into your texture. I have no idea why there is jank in the first example given that but I see jank every 6 or so images.
On the other hand, while uploading the image to the GPU was the point of ImageBitmap
, browsers are not required to upload to the GPU. ImageBitmap
is still supposed to work even if the user doesn't have a GPU. The point being it's up to the browser to decide how to implement the feature and whether it's fast or slow or jank free is entirely up to the browser.
本文标签: javascriptDecode images in web workerStack Overflow
版权声明:本文标题:javascript - Decode images in web worker - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744525281a2610708.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论