diff --git a/docs/docs/getting-started/headless.md b/docs/docs/getting-started/headless.md
index b88740e77c..6ff74ae96c 100644
--- a/docs/docs/getting-started/headless.md
+++ b/docs/docs/getting-started/headless.md
@@ -15,13 +15,16 @@ You will notice in the example below that the import URL looks different than th
```tsx
import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/commonjs/web/LoadSkiaWeb";
-import { Fill, makeOffscreenSurface, drawOffscreen } from "@shopify/react-native-skia/lib/commonjs/headless";
+import { Fill, makeOffscreenSurface, drawOffscreen, getSkiaExports } from "@shopify/react-native-skia/lib/commonjs/headless";
(async () => {
const width = 256;
const height = 256;
const r = size * 0.33;
await LoadSkiaWeb();
+ // Once that CanvasKit is loaded, you can access Skia via getSkiaExports()
+ // Alternatively you can do const {Skia} = require("@shopify/react-native-skia")
+ const {Skia} = getSkiaExports();
const surface = makeOffscreenSurface(width, height);
const image = drawOffscreen(surface,
diff --git a/package/src/__tests__/setup.ts b/package/src/__tests__/setup.ts
index 17060c5a0a..0995bbb7c2 100644
--- a/package/src/__tests__/setup.ts
+++ b/package/src/__tests__/setup.ts
@@ -25,7 +25,9 @@ export const processResult = (
surface.flush();
const image = surface.makeImageSnapshot();
surface.getCanvas().clear(Float32Array.of(0, 0, 0, 0));
- return checkImage(image, relPath, { overwrite });
+ const result = checkImage(image, relPath, { overwrite });
+ image.dispose();
+ return result;
};
interface CheckImageOptions {
diff --git a/package/src/__tests__/snapshots/leak.png b/package/src/__tests__/snapshots/leak.png
new file mode 100644
index 0000000000..ae8234bbb5
Binary files /dev/null and b/package/src/__tests__/snapshots/leak.png differ
diff --git a/package/src/headless/index.ts b/package/src/headless/index.ts
index da60492a7f..c066d1866b 100644
--- a/package/src/headless/index.ts
+++ b/package/src/headless/index.ts
@@ -7,11 +7,11 @@ import { JsiSkApi } from "../skia/web";
import { SkiaRoot } from "../renderer/Reconciler";
import { JsiDrawingContext } from "../dom/types";
import type { SkSurface } from "../skia";
+import { Skia } from "../skia/types";
export * from "../renderer/components";
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-let Skia: any;
+let Skia: Skia;
export const makeOffscreenSurface = (width: number, height: number) => {
if (!Skia) {
@@ -24,12 +24,20 @@ export const makeOffscreenSurface = (width: number, height: number) => {
return surface;
};
+export const getSkiaExports = () => {
+ if (!Skia) {
+ Skia = JsiSkApi(CanvasKit);
+ }
+ return { Skia };
+};
+
export const drawOffscreen = (surface: SkSurface, element: ReactNode) => {
const root = new SkiaRoot(Skia, false);
root.render(element);
const canvas = surface.getCanvas();
const ctx = new JsiDrawingContext(Skia, canvas);
root.dom.render(ctx);
+ root.unmount();
surface.flush();
return surface.makeImageSnapshot();
};
diff --git a/package/src/renderer/__tests__/SkiaDOM.spec.tsx b/package/src/renderer/__tests__/SkiaDOM.spec.tsx
index e00f6a2791..56620bc45e 100644
--- a/package/src/renderer/__tests__/SkiaDOM.spec.tsx
+++ b/package/src/renderer/__tests__/SkiaDOM.spec.tsx
@@ -15,8 +15,8 @@ describe("Test introductionary examples from our documentation", () => {
);
- expect(root.children().length).toBe(1);
- const child = root.children()[0]!;
+ expect(root.dom.children().length).toBe(1);
+ const child = root.dom.children()[0]!;
expect(child).toBeDefined();
expect(child.type).toBe(NodeType.Group);
expect(child.children().length).toBe(3);
@@ -31,8 +31,8 @@ describe("Test introductionary examples from our documentation", () => {
);
- expect(root.children().length).toBe(1);
- const child = root.children()[0]!;
+ expect(root.dom.children().length).toBe(1);
+ const child = root.dom.children()[0]!;
expect(child).toBeDefined();
expect(child.type).toBe(NodeType.Circle);
const circle = child;
diff --git a/package/src/renderer/__tests__/Surfaces.spec.tsx b/package/src/renderer/__tests__/Surfaces.spec.tsx
new file mode 100644
index 0000000000..89cfa5692e
--- /dev/null
+++ b/package/src/renderer/__tests__/Surfaces.spec.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+
+import { Circle, Group } from "../components";
+import { processResult } from "../../__tests__/setup";
+import { setupSkia } from "../../skia/__tests__/setup";
+
+import { drawOnNode } from "./setup";
+
+describe("Surface", () => {
+ it("A raster surface shouldn't leak (1)", () => {
+ const { Skia } = setupSkia();
+ // When leaking, the WASM memory limit will be reached quite quickly
+ // causing the test to fail
+ for (let i = 0; i < 500; i++) {
+ const surface = Skia.Surface.Make(1920, 1080)!;
+ const canvas = surface.getCanvas();
+ canvas.clear(Skia.Color("cyan"));
+ surface.flush();
+ const image = surface.makeImageSnapshot();
+ image.dispose();
+ surface.dispose();
+ }
+ });
+ it("A raster surface shouldn't leak (2)", () => {
+ for (let i = 0; i < 1000; i++) {
+ //const t = performance.now();
+ const r = 128;
+ const surface = drawOnNode(
+ <>
+
+
+
+
+
+ >
+ );
+ surface.flush();
+ //console.log(`Iteration ${i} took ${Math.floor(performance.now() - t)}ms`);
+ processResult(surface, "snapshots/leak.png");
+ surface.dispose();
+ }
+ });
+});
diff --git a/package/src/renderer/__tests__/setup.tsx b/package/src/renderer/__tests__/setup.tsx
index 52ecbf361c..089799ec74 100644
--- a/package/src/renderer/__tests__/setup.tsx
+++ b/package/src/renderer/__tests__/setup.tsx
@@ -185,8 +185,9 @@ export const height = 256 * PIXEL_RATIO;
export const center = { x: width / 2, y: height / 2 };
export const drawOnNode = (element: ReactNode) => {
- const { surface: ckSurface, draw } = mountCanvas(element);
+ const { surface: ckSurface, draw, root } = mountCanvas(element);
draw();
+ root.unmount();
return ckSurface;
};
@@ -201,7 +202,7 @@ export const mountCanvas = (element: ReactNode) => {
root.render(element);
return {
surface: ckSurface,
- root: root.dom,
+ root,
draw: () => {
const ctx = new JsiDrawingContext(Skia, canvas);
root.dom.render(ctx);
@@ -211,7 +212,7 @@ export const mountCanvas = (element: ReactNode) => {
export const serialize = (element: ReactNode) => {
const { root } = mountCanvas(element);
- const serialized = serializeNode(root);
+ const serialized = serializeNode(root.dom);
return JSON.stringify(serialized);
};
diff --git a/package/src/skia/web/JsiSkImage.ts b/package/src/skia/web/JsiSkImage.ts
index 76c307aed1..b3a522c196 100644
--- a/package/src/skia/web/JsiSkImage.ts
+++ b/package/src/skia/web/JsiSkImage.ts
@@ -45,7 +45,11 @@ export const toBase64String = (bytes: Uint8Array) => {
};
export class JsiSkImage extends HostObject implements SkImage {
- constructor(CanvasKit: CanvasKit, ref: Image) {
+ constructor(
+ CanvasKit: CanvasKit,
+ ref: Image,
+ private releaseCtx?: () => void
+ ) {
super(CanvasKit, ref, "Image");
}
@@ -128,6 +132,8 @@ export class JsiSkImage extends HostObject implements SkImage {
return toBase64String(bytes);
}
+ // TODO: this is leaking on Web
+ // Add signature with allocated buffer
readPixels(srcX?: number, srcY?: number, imageInfo?: ImageInfo) {
const info = this.getImageInfo();
const pxInfo: CKImageInfo = {
@@ -148,6 +154,9 @@ export class JsiSkImage extends HostObject implements SkImage {
dispose = () => {
this.ref.delete();
+ if (this.releaseCtx) {
+ this.releaseCtx();
+ }
};
makeNonTextureImage(): SkImage {
@@ -157,7 +166,16 @@ export class JsiSkImage extends HostObject implements SkImage {
...partialInfo,
colorSpace,
};
- const pixels = this.ref.readPixels(0, 0, info) as Uint8Array | null;
+
+ var pixelLen = info.width * info.height * 4;
+ const pixelPtr = this.CanvasKit.Malloc(Uint8Array, pixelLen);
+ const pixels = this.ref.readPixels(
+ 0,
+ 0,
+ info,
+ pixelPtr,
+ info.width * 4
+ ) as Uint8Array | null;
if (!pixels) {
throw new Error("Could not create image from bytes");
}
@@ -165,6 +183,8 @@ export class JsiSkImage extends HostObject implements SkImage {
if (!img) {
throw new Error("Could not create image from bytes");
}
- return new JsiSkImage(this.CanvasKit, img);
+ return new JsiSkImage(this.CanvasKit, img, () => {
+ this.CanvasKit.Free(pixelPtr);
+ });
}
}
diff --git a/package/src/skia/web/JsiSkSurface.ts b/package/src/skia/web/JsiSkSurface.ts
index 884678476c..a5e2c5c0ad 100644
--- a/package/src/skia/web/JsiSkSurface.ts
+++ b/package/src/skia/web/JsiSkSurface.ts
@@ -11,12 +11,19 @@ export class JsiSkSurface
extends HostObject
implements SkSurface
{
- constructor(CanvasKit: CanvasKit, ref: Surface) {
+ constructor(
+ CanvasKit: CanvasKit,
+ ref: Surface,
+ private releaseCtx?: () => void
+ ) {
super(CanvasKit, ref, "Surface");
}
dispose = () => {
this.ref.delete();
+ if (this.releaseCtx) {
+ this.releaseCtx();
+ }
};
flush() {
diff --git a/package/src/skia/web/JsiSkSurfaceFactory.ts b/package/src/skia/web/JsiSkSurfaceFactory.ts
index 57e3813c3b..32395aab28 100644
--- a/package/src/skia/web/JsiSkSurfaceFactory.ts
+++ b/package/src/skia/web/JsiSkSurfaceFactory.ts
@@ -11,11 +11,26 @@ export class JsiSkSurfaceFactory extends Host implements SurfaceFactory {
}
Make(width: number, height: number) {
- const surface = this.CanvasKit.MakeSurface(width, height);
+ var pixelLen = width * height * 4;
+ const pixelPtr = this.CanvasKit.Malloc(Uint8Array, pixelLen);
+ const surface = this.CanvasKit.MakeRasterDirectSurface(
+ {
+ width: width,
+ height: height,
+ colorType: this.CanvasKit.ColorType.RGBA_8888,
+ alphaType: this.CanvasKit.AlphaType.Unpremul,
+ colorSpace: this.CanvasKit.ColorSpace.SRGB,
+ },
+ pixelPtr,
+ width * 4
+ );
if (!surface) {
return null;
}
- return new JsiSkSurface(this.CanvasKit, surface);
+ surface.getCanvas().clear(this.CanvasKit.TRANSPARENT);
+ return new JsiSkSurface(this.CanvasKit, surface, () => {
+ this.CanvasKit.Free(pixelPtr);
+ });
}
MakeOffscreen(width: number, height: number) {