Web worker and texture transfer

My code is very big. Otherwise I would have put it in here.
I’ve been dealing with web workers for a while. I’m amazed how much more performance is connected with it. Yesterday, after several days of unsuccessful attempts, I realized that I can’t transfer textures to web workers. I’m also not doing myself any favors by handing over textures that have been converted to html images. This works, but again it is very slow.
This is a bit frustrating because I don’t know how to implement my idea without the web worker handing over large images.
Is there a way to efficiently pass images to web workers so that they can be passed on to the working code of each thread?

code from simon dev “simondevyoutube (simondevyoutube) / Repositories · GitHub” ProceduralTerrain_Part10.
The code is already very extensive but much smaller than mine. If you want to run this, you have to replace the sharedBufferArrays with BufferArrays in “terrain-builder-threaded-worker.js”. The workers are managed by “terrain-builder-threaded.js”. And this in turn is called by “terrain.js” and it also gets its params from this. My goal is to get textures from “terrain.js” into the workers.
“terrain.js” → pass textures to “terrain-builder-threaded.js” → pass textures to the several threads “terrain-builder-threaded-worker.js”
below the code of “terrain-builder-threaded.js”

import {terrain_chunk} from './terrain-chunk.js';


export const terrain_builder_threaded = (function() {

  const _NUM_WORKERS = 7;

  let _IDs = 0;

  class WorkerThread {
    constructor(s) {
      this.worker_ = new Worker(s, {type: 'module'});
      this.worker_.onmessage = (e) => {
        this._OnMessage(e);
      };
      this._resolve = null;
      this._id = _IDs++;
    }

    _OnMessage(e) {
      const resolve = this._resolve;
      this._resolve = null;
      resolve(e.data);
    }

    get id() {
      return this._id;
    }

    postMessage(s, resolve) {
      this._resolve = resolve;
      this.worker_.postMessage(s);
    }
  }

  class WorkerThreadPool {
    constructor(sz, entry) {
      this.workers_ = [...Array(sz)].map(_ => new WorkerThread(entry));
      this.free_ = [...this.workers_];
      this.busy_ = {};
      this.queue_ = [];
    }

    get length() {
      return this.workers_.length;
    }

    get Busy() {
      return this.queue_.length > 0 || Object.keys(this.busy_).length > 0;
    }

    Enqueue(workItem, resolve) {
      this.queue_.push([workItem, resolve]);
      this._PumpQueue();
    }

    _PumpQueue() {
      while (this.free_.length > 0 && this.queue_.length > 0) {
        const w = this.free_.pop();
        this.busy_[w.id] = w;

        const [workItem, workResolve] = this.queue_.shift();

        w.postMessage(workItem, (v) => {
          delete this.busy_[w.id];
          this.free_.push(w);
          workResolve(v);
          this._PumpQueue();
        });
      }
    }
  }

  class _TerrainChunkRebuilder_Threaded {
    constructor(params) {
      this.pool_ = {};
      this.old_ = [];

      this.workerPool_ = new WorkerThreadPool(
        _NUM_WORKERS, 'src/terrain-builder-threaded-worker.js');
  
      this.params_ = params;
    }

    _OnResult(chunk, msg) {
      if (msg.subject == 'build_chunk_result') {
        chunk.RebuildMeshFromData(msg.data);
      } else if (msg.subject == 'quick_rebuild_chunk_result') {
        chunk.QuickRebuildMeshFromData(msg.data);
      }
    }

    AllocateChunk(params) {
      const w = params.width;

      if (!(w in this.pool_)) {
        this.pool_[w] = [];
      }

      let c = null;
      if (this.pool_[w].length > 0) {
        c = this.pool_[w].pop();
        c.params_ = params;
      } else {
        c = new terrain_chunk.TerrainChunk(params);
      }

      c.Hide();

      const threadedParams = {
        noiseParams: params.noiseParams,
        colourNoiseParams: params.colourNoiseParams,
        biomesParams: params.biomesParams,
        colourGeneratorParams: params.colourGeneratorParams,
        heightGeneratorsParams: params.heightGeneratorsParams,
        width: params.width,
        neighbours: params.neighbours,
        offset: params.offset.toArray(),
        origin: params.origin.toArray(),
        radius: params.radius,
        resolution: params.resolution,
        worldMatrix: params.transform,
        textureMaps: //here i want to transfer textures to the threads
      };

      const msg = {
        subject: 'build_chunk',
        params: threadedParams,
      };

      this.workerPool_.Enqueue(msg, (m) => {
        this._OnResult(c, m);
      });

      return c;    
    }

    RetireChunks(chunks) {
      this.old_.push(...chunks);
    }

    _RecycleChunks(chunks) {
      for (let c of chunks) {
        if (!(c.chunk.params_.width in this.pool_)) {
          this.pool_[c.chunk.params_.width] = [];
        }

        c.chunk.Destroy();
      }
    }

    get Busy() {
      return this.workerPool_.Busy;
    }

    Rebuild(chunks) {
      for (let k in chunks) {
        this.workerPool_.Enqueue(chunks[k].chunk.params_);
      }
    }

    QuickRebuild(chunks) {
      for (let k in chunks) {
        const chunk = chunks[k];
        const params = chunk.chunk.params_;

        const threadedParams = {
          noiseParams: params.noiseParams,
          colourNoiseParams: params.colourNoiseParams,
          biomesParams: params.biomesParams,
          colourGeneratorParams: params.colourGeneratorParams,
          heightGeneratorsParams: params.heightGeneratorsParams,
          width: params.width,
          neighbours: params.neighbours,
          offset: params.offset.toArray(),
          origin: params.origin.toArray(),
          radius: params.radius,
          resolution: params.resolution,
          worldMatrix: params.transform,
          textureMaps: //here i want to transfer textures to the threads
        };

        const msg = {
          subject: 'rebuild_chunk',
          params: threadedParams,
          mesh: chunk.chunk.rebuildData_,
        };
  
        this.workerPool_.Enqueue(msg, (m) => {
          this._OnResult(chunk.chunk, m);
        });
      }
    }

    Update() {
      if (!this.Busy) {
        this._RecycleChunks(this.old_);
        this.old_ = [];
      }
    }
  }

  return {
    TerrainChunkRebuilder_Threaded: _TerrainChunkRebuilder_Threaded
  }
})();

Is OffscreenCanvas the solution? Does anyone have an example with textures passed and multithreading?

I think the correct method is to pass SharedBufferArrays, though I’ve not used them yet.

SharedBufferArrays are not necessary, normal BufferArrays work the same way. SharedBufferArrays are more memory efficient but I don’t know how to use them yet. As I now know, workers can only receive and return simple data structures. Therefore passing textures does not work. I guess that’s why OffscreenCanvas was introduced because they can be passed to worker. I have no experience with it yet. I’m going to experiment a bit. Does anyone have experience with multithreading and OffscreenCanvas?

I found this interesting link: https://levelup.gitconnected.com/improve-javascript-performance-with-offscreencanvas-1180dc5376e9 This looks very much like what i need. I hope so.

So a couple things:

Shared arrayBuffers are a way for workers to exchange memory significantly faster/better.

The challenge is for security reasons the host has to provide specific security headers in the responses, and be on the same domain.
(See link for details)

If you try a sandbox or generic setup the sharedarray will fail…

Simons example doesn’t pass textures (I don’t think :thinking:) but terrain mesh data.

If you want to pass something to a worker you can hand it off, but I don’t think a texture object can be transferred, but a arrayBuffer should be able to.

There’s cloning (takes the data and copies it, probably what your html image is doing) and transfering, which is like a one way message…

See this: Using Web Workers - Web APIs | MDN

For a full walkthrough of workers and a few examples of passing data

I still have to look at the SharedBufferArray because it doesn’t work for me. That’s why I’ve only used BufferArrays so far.
But I solved the problem with image transfer. In the description of OffscreenCanvas I read that it is intended and works for such purposes (use with workers). Interestingly, the whole thing is very graphics card-heavy. I would have expected the CPU to do more since it’s not a shader code but something in Three.js (Javascript) code. But the CPU has little to do with it. Does anyone know anything about how OffscreenCanvas works? I’m very happy that it works so well, but I also want to get to understand how it works in the hardware.

instead this:

GetImageData: function(image) {
	const canvas = document.createElement('canvas');
	canvas.width = image.width;
	canvas.height = image.height;
		
	const context = canvas.getContext( '2d' );
	context.drawImage(image, 0, 0);
		
	return context.getImageData(0, 0, image.width, image.height);
},

i use this:

GetOffscreenImageData: function(image) {		
	const canvas = new OffscreenCanvas(image.width, image.height);
	const context = canvas.getContext('2d');
	context.drawImage( image, 0, 0 );

	return context.getImageData( 0, 0, image.width, image.height );
}

So workers in connection with textures require OffscreenCanvas to work efficiently or at all. Together, worker and OffscreenCanvas are a powerful combination. I am happy :partying_face:

1 Like