[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 };
}
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();

View File

@ -33,6 +33,8 @@ const renderers = new Map<string, {
imgSize: number;
reduceFactor: number;
handle: any;
imageData: ImageData;
customColor?: [number, number, number];
}>();
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];
}
}

View File

@ -25,7 +25,7 @@ export default function createInterface(api: Record<string, Function>) {
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<string, Function>) {
};
}
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);
}