1

I’m trying to use a web-worker to render a scene with threejs. I want to render some dynamic font using a CanvasTexture. But I find that when I use canvas in web-worker from transferControlToOffscreen, if I change the text, the render loop will stop, but this doesn't happen with new OffscreenCanvas().

//if use new OffscreenCanvas, render loop will work fine.
//if not, canvas2 is transferd from main thread, when change the text, render loop will stop sometimes
//canvas2=new OffscreenCanvas(200,200);  
canvas2.height = 200;
canvas2.width = 200;
let context = canvas2.getContext('2d');
context.font = "40px Helvetica";
context.fillStyle = 'red';
context.fillText("123", canvas2.width / 2, canvas2.height / 2);
this.sprite = new THREE.Sprite(new THREE.SpriteMaterial({map: new THREE.CanvasTexture(canvas2)}));
this.sprite.scale.set(50, 50, 50);
this.scene.add(this.sprite);

If I declare a canvas in the HTML and simply hide it, after transfered to worker, the problem doesn’t occur.

<canvas id="canvas3"style="display: none;" ></canvas>
// spriteCanvas2 = document.createElement('canvas').transferControlToOffscreen();
spriteCanvas2=document.getElementById('canvas3').transferControlToOffscreen();

The live sample is here.The top canvas run in main thread and the bottom run in worker. If click the bottom canvas, it will stop render sometimes. Some additional information is in the forum.

If run in local not codepen, it will trigger consistently in three clicks. If not trigger, please refresh the browser.

Could anyone tell the difference between them? Thanks in advance.

7
  • Which browser are you using? The codepen renders 2 similar animations for me. Commented Sep 5 at 3:10
  • @Kaiido Chrome 140.0.7339.80. I also trigger it in 96.0.4664.93 The bottom canvas render CanvasTexture with transferControlToOffscreen. If click the bottom canvas, it will stop render sometimes. If not, please refresh the browser. Commented Sep 5 at 3:15
  • That would be a Chrome bug. Open an issue at issues.chromium.org/issues/wizard The animation frame handler is stuck, calling requestAnimationFrame(() => () => console.log("aa")) from the Worker's console never calls the callback. Removing the <canvas> from the main thread will unlock the rAF calls. Educated guess: they lock on the rendering pipeline and missed a message somewhere. However for me it's a 1 / 100 or so repro, not 1/3 at all. Commented Sep 5 at 4:02
  • @Kaiido Thanks, I will open an issue. After saving the code locally and running it, I can reproduce the issue very easily. Usually it happens within just a few clicks; if not, refreshing the page and trying again will trigger it. Commented Sep 5 at 4:23
  • I found the issue. If you haven't yet and if you wish, I can open the bug report. Commented Sep 5 at 5:29

1 Answer 1

1

This is a Chrome bug, caused by the Garbage Collection of the spriteCanvas2's original <canvas>.
Indeed, when you do spriteCanvas2=document.createElement('canvas').transferControlToOffscreen();, the intermediary <canvas> returned by document.createElement('canvas') isn't held by anything, and thus, it's marked as being garbage collectable.

The Worker's side animation frame provider will lock onto the last draw commands have been passed to the placeholder canvas's buffer (the one in the main thread). Since it doesn't exist anymore, it's stuck.

This has nothing to do with Three.js and can be reproduced in a more minimal test-case:

const workerScript = `
// If we didn't run rAF in the last second, the renderer is locked
const notifyDeath = () => setTimeout(() => postMessage("dead"), 1000);

self.onmessage = ({ data: canvas }) => {
  const ctx = canvas.getContext("2d");
  let timer = notifyDeath();
  const anim = () => {
    clearTimeout(timer);
    timer = notifyDeath();
    ctx.clearRect(0, 0, 300, 150);
    requestAnimationFrame(anim);
  }
  requestAnimationFrame(anim);
};
`;
const workerURL = URL.createObjectURL(new Blob([workerScript], { type: "text/javascript" }));
const worker = new Worker(workerURL);
worker.onerror = console.error;
// get a WeakRef from the <canvas> element
const weakCanvas = new WeakRef(document.createElement("canvas"));
const offscreen = weakCanvas.deref()
  .transferControlToOffscreen();
worker.postMessage(offscreen, [offscreen]);
// 
worker.onmessage = ({ data }) => {
  console.log("message from worker:", { data }); // died
  document.body.style.background = "red";
  console.log("is still reffed?", weakCanvas.deref() !== undefined); // undefined
}
// Force garbage production
onclick = () => {
  const a = Array.from({ length: 10e5 }, () => ({ a: Math.random() }));
}
You may have to wait quite some time before the object is dereferenced.<br>
You may click anywhere in this frame to try to force garbage production.<br>
Beware, switching tab might trigger the check too, since rAF is throttled in that case.

So to workaround it, you could keep a reference to the <canvas>, or use an OffscreenCanvas directly since you won't ever render that <canvas> anyway.

Sign up to request clarification or add additional context in comments.

2 Comments

Yeah!! When weakCanvas.deref() return undefined, the render loop indeed stop. I had actually anticipated this scenario earlier, but a coding mistake later threw me off. Thank you very much!
Would using an OffscreenCanvas like this in three.js give me the ability to wait for the canvas to repaint on each iteration of my render loop? Kaiido, let me know if you can take a look at my question here: stackoverflow.com/questions/79786985

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.