From 572fb78daafa31b066db24a617f108f2b928b647 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 13 Nov 2022 17:06:02 +0400 Subject: [PATCH] [Perf] RLottie: Add GPU-acceleration with `ImageBitmap` --- src/lib/rlottie/RLottie.ts | 39 +++++++++++++++++-------------- src/lib/rlottie/rlottie.worker.ts | 37 +++++++++++++++++++++++++---- src/util/createWorkerInterface.ts | 12 ++++++---- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index 2d98088a2..c298a090b 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -16,8 +16,8 @@ interface Params { coords?: { x: number; y: number }; } -type Frames = ArrayBuffer[]; -type Chunks = (Frames | undefined)[]; +type Frame = ImageBitmap; +type Chunks = (Frame[] | undefined)[]; // TODO Consider removing chunks const CHUNK_SIZE = 1; @@ -49,6 +49,8 @@ class RLottie { private imgSize!: number; + private imageData!: ImageData; + private msPerFrame = 1000 / 60; private reduceFactor = 1; @@ -263,6 +265,7 @@ class RLottie { if (!this.imgSize) { this.imgSize = imgSize; + this.imageData = new ImageData(imgSize, imgSize); } if (this.isRendererInited) { @@ -299,6 +302,7 @@ class RLottie { this.tgsUrl, this.imgSize, this.params.isLowPriority, + this.customColor, this.onRendererInit.bind(this), ], }); @@ -407,26 +411,14 @@ class RLottie { return false; } - const arr = new Uint8ClampedArray(frame); - if (this.customColor) { - for (let i = 0; i < arr.length; i += 4) { - /* eslint-disable prefer-destructuring */ - arr[i] = this.customColor[0]; - arr[i + 1] = this.customColor[1]; - arr[i + 2] = this.customColor[2]; - /* eslint-enable prefer-destructuring */ - } - } - - const imageData = new ImageData(arr, this.imgSize, this.imgSize); - this.containers.forEach((containerData) => { const { ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad, } = containerData; if (!isLoaded || !isPaused) { - ctx.putImageData(imageData, x || 0, y || 0); + ctx.clearRect(x || 0, y || 0, this.imgSize, this.imgSize); + ctx.drawImage(frame, x || 0, y || 0); } if (!isLoaded) { @@ -511,6 +503,17 @@ class RLottie { return chunk[indexInChunk]; } + private setFrame(frameIndex: number, frame: Frame) { + const chunkIndex = this.getChunkIndex(frameIndex); + const indexInChunk = this.getFrameIndexInChunk(frameIndex); + const chunk = this.chunks[chunkIndex]; + if (!chunk) { + return; + } + + chunk[indexInChunk] = frame; + } + private getFrameIndexInChunk(frameIndex: number) { const chunkIndex = this.getChunkIndex(frameIndex); return frameIndex - chunkIndex * this.chunkSize; @@ -557,7 +560,7 @@ class RLottie { } } - private onFrameLoad(frameIndex: number, arrayBuffer: ArrayBuffer) { + private onFrameLoad(frameIndex: number, imageBitmap: ImageBitmap) { const chunkIndex = this.getChunkIndex(frameIndex); const chunk = this.chunks[chunkIndex]; // Frame can be skipped and chunk can be already cleaned up @@ -565,7 +568,7 @@ class RLottie { return; } - chunk[this.getFrameIndexInChunk(frameIndex)] = arrayBuffer; + chunk[this.getFrameIndexInChunk(frameIndex)] = imageBitmap; if (this.isWaiting) { this.doPlay(); diff --git a/src/lib/rlottie/rlottie.worker.ts b/src/lib/rlottie/rlottie.worker.ts index 7c9ade402..0a359080d 100644 --- a/src/lib/rlottie/rlottie.worker.ts +++ b/src/lib/rlottie/rlottie.worker.ts @@ -33,6 +33,8 @@ const renderers = new Map(); async function init( @@ -40,6 +42,7 @@ async function init( tgsUrl: string, imgSize: number, isLowPriority: boolean, + customColor: [number, number, number] | undefined, onInit: CancellableCallback, ) { if (!rLottieApi) { @@ -52,9 +55,14 @@ async function init( const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap); rLottieApi.resize(handle, imgSize, imgSize); + const imageData = new ImageData(imgSize, imgSize); + const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount); - renderers.set(key, { imgSize, reduceFactor, handle }); + renderers.set(key, { + imgSize, reduceFactor, handle, imageData, customColor, + }); + onInit(reduceFactor, msPerFrame, reducedFramesCount); } @@ -74,6 +82,7 @@ async function changeData( const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap); const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount); + onInit(reduceFactor, msPerFrame, reducedFramesCount); } @@ -110,7 +119,9 @@ async function renderFrames( await rLottieApiPromise; } - const { imgSize, reduceFactor, handle } = renderers.get(key)!; + const { + imgSize, reduceFactor, handle, imageData, customColor, + } = renderers.get(key)!; for (let i = fromIndex; i <= toIndex; i++) { const realIndex = i * reduceFactor; @@ -118,8 +129,26 @@ async function renderFrames( rLottieApi.render(handle, realIndex); const bufferPointer = rLottieApi.buffer(handle); const data = Module.HEAPU8.subarray(bufferPointer, bufferPointer + (imgSize * imgSize * 4)); - const arrayBuffer = new Uint8ClampedArray(data).buffer; - onProgress(i, arrayBuffer); + + if (customColor) { + const arr = new Uint8ClampedArray(data); + applyColor(arr, customColor); + imageData.data.set(arr); + } else { + imageData.data.set(data); + } + + const imageBitmap = await createImageBitmap(imageData); + + onProgress(i, imageBitmap); + } +} + +function applyColor(arr: Uint8ClampedArray, color: [number, number, number]) { + for (let i = 0; i < arr.length; i += 4) { + arr[i] = color[0]; + arr[i + 1] = color[1]; + arr[i + 2] = color[2]; } } diff --git a/src/util/createWorkerInterface.ts b/src/util/createWorkerInterface.ts index f36a4595a..486872301 100644 --- a/src/util/createWorkerInterface.ts +++ b/src/util/createWorkerInterface.ts @@ -25,7 +25,7 @@ export default function createInterface(api: Record) { type: 'methodCallback', messageId, callbackArgs, - }, lastArg instanceof ArrayBuffer ? [lastArg] : undefined); + }, isTransferable(lastArg) ? [lastArg] : undefined); }; callbackState.set(messageId, callback); @@ -78,6 +78,10 @@ export default function createInterface(api: Record) { }; } +function isTransferable(obj: any) { + return obj instanceof ArrayBuffer || obj instanceof ImageBitmap; +} + function handleErrors() { self.onerror = (e) => { // eslint-disable-next-line no-console @@ -92,9 +96,9 @@ function handleErrors() { }); } -function sendToOrigin(data: WorkerMessageData, arrayBuffers?: ArrayBuffer[]) { - if (arrayBuffers) { - postMessage(data, arrayBuffers); +function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) { + if (transferables) { + postMessage(data, transferables); } else { postMessage(data); }