[Perf] RLottie: Add GPU-acceleration with ImageBitmap

This commit is contained in:
Alexander Zinchuk 2022-11-13 17:06:02 +04:00
parent a5cda0f209
commit 572fb78daa
3 changed files with 62 additions and 26 deletions

View File

@ -16,8 +16,8 @@ interface Params {
coords?: { x: number; y: number }; coords?: { x: number; y: number };
} }
type Frames = ArrayBuffer[]; type Frame = ImageBitmap;
type Chunks = (Frames | undefined)[]; type Chunks = (Frame[] | undefined)[];
// TODO Consider removing chunks // TODO Consider removing chunks
const CHUNK_SIZE = 1; const CHUNK_SIZE = 1;
@ -49,6 +49,8 @@ class RLottie {
private imgSize!: number; private imgSize!: number;
private imageData!: ImageData;
private msPerFrame = 1000 / 60; private msPerFrame = 1000 / 60;
private reduceFactor = 1; private reduceFactor = 1;
@ -263,6 +265,7 @@ class RLottie {
if (!this.imgSize) { if (!this.imgSize) {
this.imgSize = imgSize; this.imgSize = imgSize;
this.imageData = new ImageData(imgSize, imgSize);
} }
if (this.isRendererInited) { if (this.isRendererInited) {
@ -299,6 +302,7 @@ class RLottie {
this.tgsUrl, this.tgsUrl,
this.imgSize, this.imgSize,
this.params.isLowPriority, this.params.isLowPriority,
this.customColor,
this.onRendererInit.bind(this), this.onRendererInit.bind(this),
], ],
}); });
@ -407,26 +411,14 @@ class RLottie {
return false; 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) => { this.containers.forEach((containerData) => {
const { const {
ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad, ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad,
} = containerData; } = containerData;
if (!isLoaded || !isPaused) { 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) { if (!isLoaded) {
@ -511,6 +503,17 @@ class RLottie {
return chunk[indexInChunk]; 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) { private getFrameIndexInChunk(frameIndex: number) {
const chunkIndex = this.getChunkIndex(frameIndex); const chunkIndex = this.getChunkIndex(frameIndex);
return frameIndex - chunkIndex * this.chunkSize; 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 chunkIndex = this.getChunkIndex(frameIndex);
const chunk = this.chunks[chunkIndex]; const chunk = this.chunks[chunkIndex];
// Frame can be skipped and chunk can be already cleaned up // Frame can be skipped and chunk can be already cleaned up
@ -565,7 +568,7 @@ class RLottie {
return; return;
} }
chunk[this.getFrameIndexInChunk(frameIndex)] = arrayBuffer; chunk[this.getFrameIndexInChunk(frameIndex)] = imageBitmap;
if (this.isWaiting) { if (this.isWaiting) {
this.doPlay(); this.doPlay();

View File

@ -33,6 +33,8 @@ const renderers = new Map<string, {
imgSize: number; imgSize: number;
reduceFactor: number; reduceFactor: number;
handle: any; handle: any;
imageData: ImageData;
customColor?: [number, number, number];
}>(); }>();
async function init( async function init(
@ -40,6 +42,7 @@ async function init(
tgsUrl: string, tgsUrl: string,
imgSize: number, imgSize: number,
isLowPriority: boolean, isLowPriority: boolean,
customColor: [number, number, number] | undefined,
onInit: CancellableCallback, onInit: CancellableCallback,
) { ) {
if (!rLottieApi) { if (!rLottieApi) {
@ -52,9 +55,14 @@ async function init(
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap); const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
rLottieApi.resize(handle, imgSize, imgSize); rLottieApi.resize(handle, imgSize, imgSize);
const imageData = new ImageData(imgSize, imgSize);
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount); 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); onInit(reduceFactor, msPerFrame, reducedFramesCount);
} }
@ -74,6 +82,7 @@ async function changeData(
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap); const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount); const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount);
onInit(reduceFactor, msPerFrame, reducedFramesCount); onInit(reduceFactor, msPerFrame, reducedFramesCount);
} }
@ -110,7 +119,9 @@ async function renderFrames(
await rLottieApiPromise; 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++) { for (let i = fromIndex; i <= toIndex; i++) {
const realIndex = i * reduceFactor; const realIndex = i * reduceFactor;
@ -118,8 +129,26 @@ async function renderFrames(
rLottieApi.render(handle, realIndex); rLottieApi.render(handle, realIndex);
const bufferPointer = rLottieApi.buffer(handle); const bufferPointer = rLottieApi.buffer(handle);
const data = Module.HEAPU8.subarray(bufferPointer, bufferPointer + (imgSize * imgSize * 4)); 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];
} }
} }

View File

@ -25,7 +25,7 @@ export default function createInterface(api: Record<string, Function>) {
type: 'methodCallback', type: 'methodCallback',
messageId, messageId,
callbackArgs, callbackArgs,
}, lastArg instanceof ArrayBuffer ? [lastArg] : undefined); }, isTransferable(lastArg) ? [lastArg] : undefined);
}; };
callbackState.set(messageId, callback); callbackState.set(messageId, callback);
@ -78,6 +78,10 @@ export default function createInterface(api: Record<string, Function>) {
}; };
} }
function isTransferable(obj: any) {
return obj instanceof ArrayBuffer || obj instanceof ImageBitmap;
}
function handleErrors() { function handleErrors() {
self.onerror = (e) => { self.onerror = (e) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -92,9 +96,9 @@ function handleErrors() {
}); });
} }
function sendToOrigin(data: WorkerMessageData, arrayBuffers?: ArrayBuffer[]) { function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) {
if (arrayBuffers) { if (transferables) {
postMessage(data, arrayBuffers); postMessage(data, transferables);
} else { } else {
postMessage(data); postMessage(data);
} }