class GpuImageTracer {
static {
GpuImageTracer.maxWorkerCount = Math.max(1, navigator.hardwareConcurrency - 1)
}
constructor(options) {
options = options ?? {};
this.colorTable = options.colorTable;
this.onTileProcessed = options.fnOnTileProcessed ?? (() => true);
this.maxTileSize = options.maxTileSize;
this.coordClasses = new MaskMap(CoordMask);
const pixelVectors = this.pixelVectors = new Map();
pixelVectors.getOrCreate = (key) => {
let maskSet = pixelVectors.get(key);
if (!maskSet) pixelVectors.set(key, maskSet = new MaskSet(XydMask));
return maskSet;
}
}
async traceImage(image) {
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
const tsOptions = {};
if (this.maxTileSize) tsOptions.maxTileSize = this.maxTileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);
const tileRects = tileSlicer.getTileRects();
const workers = [];
{
const start = performance.now();
const workerPromises = [];
const workerCount = Math.min(tileRects.length, GpuImageTracer.maxWorkerCount);
for (let i = 0; i < workerCount; i++) {
const p = getWorker(tileSlicer.baseTileSize, this.colorTable).then(w => workers.push(w));
workerPromises.push(p);
}
await Promise.all(workerPromises);
console.log(workerCount + ' workers initialized', performance.now() - start);
}
const promises = new Set();
const busyWorkers = new Set();
// process tiles
for (let rect of tileSlicer.getTileRects()) {
while (busyWorkers.size >= workers.length) {
await Promise.race([...promises]);
}
const freeWorker = workers.find(w => !busyWorkers.has(w));
busyWorkers.add(freeWorker);
const bitmap = await tileSlicer.getTileBitmap(rect);
let promise = freeWorker.gpuReadTile(bitmap, rect).then(r => {
promises.delete(promise);
busyWorkers.delete(freeWorker);
this._ingestGpuTileResult(r);
new Promise(() => this.onTileProcessed(rect.unpadded));
bitmap.close();
});
promises.add(promise);
await pauseIfNeeded();
}
await Promise.all([...promises]);
tileSlicer.destroy();
await pauseIfNeeded();
workers.forEach(w => w.terminate());
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}
async traceImageSerial(image) {
// image
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
// tileSlicer
const tsOptions = {};
if (this.maxTileSize) tsOptions.maxTileSize = this.maxTileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);
// reglProgram
const reglProgram = getReglProgram(tileSlicer.baseTileSize, this.colorTable);
// process tiles
for (let rect of tileSlicer.getTileRects()) {
const bitmap = await tileSlicer.getTileBitmap(rect);
const gpuOutput = await gpuProcessTile(bitmap, rect, reglProgram);
bitmap.close();
const tileResult = {};
tileResult.pixelVectors = await extractPixelVectors(gpuOutput);
await pauseIfNeeded();
tileResult.coordClasses = await extractCoordClasses(gpuOutput);
await pauseIfNeeded();
this._ingestGpuTileResult(tileResult);
await pauseIfNeeded();
new Promise(() => this.onTileProcessed(rect.unpadded));
}
tileSlicer.destroy();
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}
_ingestGpuTileResult(tileResult) {
for (let [colorNum, newVectors] of tileResult.pixelVectors) {
this.pixelVectors.getOrCreate(colorNum).addValues(newVectors);
}
for (let [coord, coordClass] of tileResult.coordClasses) {
this.coordClasses.set(coord, coordClass);
}
}
async _buildRings() {
const polygonRings = await pixelVectorsToRings(this.pixelVectors, this.coordClasses);
const bbox = [0, 0, ...this.imageSize];
return { bbox, polygonRings, coordClasses: this.coordClasses };
}
}