Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions apps/example/src/components/workletSerialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ export interface RoundTripResult {
error?: string;
}

export interface BitmapRoundTripResult {
width: number;
height: number;
isImageBitmap: boolean;
sameObjectBack: boolean;
// Set when the worklet threw, with the error message.
error?: string;
}

export interface CanvasRoundTripResult {
// The width the worklet read, before setting it to width + 1.
width: number;
sameObjectBack: boolean;
// Set when the worklet threw, with the error message.
error?: string;
}

export interface CaptureResult {
threw: boolean;
message: string;
Expand Down Expand Up @@ -112,6 +129,80 @@ export const workletSerialization = {
);
}),

// Sends `bitmap` to a worklet runtime and back, as roundTripBuffer does
// for a buffer. Unboxing it there rebuilds it from its class brand.
roundTripImageBitmap: (bitmap: ImageBitmap): Promise<BitmapRoundTripResult> =>
new Promise((resolve) => {
const done = (
onWorklet: { width: number; height: number; isImageBitmap: boolean },
back: ImageBitmap,
) => {
resolve({ ...onWorklet, sameObjectBack: back === bitmap });
};
const failed = (error: string) => {
resolve({
width: -1,
height: -1,
isImageBitmap: false,
sameObjectBack: false,
error,
});
};
scheduleOnRuntime(
getRuntime(),
(image: ImageBitmap) => {
"worklet";
try {
installWebGPU();
// Read through globalThis: the worklets babel plugin would
// otherwise capture the main runtime's constructor by closure.
const ctor = globalThis.ImageBitmap;
const onWorklet = {
width: image.width,
height: image.height,
isImageBitmap:
typeof ctor === "function" && image instanceof ctor,
};
scheduleOnRN(done, onWorklet, image);
} catch (e) {
scheduleOnRN(failed, String(e));
}
},
bitmap,
);
}),

// Sends a native Canvas (RNWebGPU.getNativeSurface(), GPUCanvasContext's
// `canvas`) to a worklet runtime and back. Canvas has no global constructor
// in installConstructors, so this checks that unboxing does not depend on
// that list. Sets `width` there, on the native object the main runtime
// also reads.
roundTripCanvas: (canvas: {
width: number;
}): Promise<CanvasRoundTripResult> =>
new Promise((resolve) => {
const done = (onWorklet: { width: number }, back: { width: number }) => {
resolve({ ...onWorklet, sameObjectBack: back === canvas });
};
const failed = (error: string) => {
resolve({ width: -1, sameObjectBack: false, error });
};
scheduleOnRuntime(
getRuntime(),
(c: { width: number }) => {
"worklet";
try {
const onWorklet = { width: c.width };
c.width = onWorklet.width + 1;
scheduleOnRN(done, onWorklet, c);
} catch (e) {
scheduleOnRN(failed, String(e));
}
},
canvas,
);
}),

// Calls installWebGPU() on a worklet runtime nothing else was installed on
// and reports the class constructors it finds there (see
// ConstructorsResult).
Expand Down
74 changes: 42 additions & 32 deletions packages/webgpu/cpp/jsi/NativeObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ class NativeObject : public NativeObjectBase,
return; // Already installed on this runtime
}

// Every wrapper is made by create(), which installs the prototype first,
// so the class is registered before any of its objects can be boxed,
// whether or not its constructor is ever installed as a global.
registerReconstructor();

// Create prototype object
jsi::Object prototype(runtime);

Expand Down Expand Up @@ -411,40 +416,10 @@ class NativeObject : public NativeObjectBase,
* Idempotent per runtime: once the global exists there, later calls return
* without touching it, so the constructor object keeps its identity.
*
* Also registers this class with NativeObjectRegistry so that
* BoxedWebGPUObject::unbox() can rebuild its objects on secondary runtimes.
* Not needed for objects to cross runtimes: installPrototype() registers
* the class with NativeObjectRegistry.
*/
static void installConstructor(jsi::Runtime &runtime) {
// Register the reconstructor used by BoxedWebGPUObject::unbox() to
// rebuild this object (prototype + native state) on another runtime.
static std::once_flag registryFlag;
std::call_once(registryFlag, []() {
NativeObjectRegistry::getInstance().registerClass(
Derived::CLASS_NAME,
[](jsi::Runtime &rt,
std::shared_ptr<jsi::NativeState> state) -> jsi::Value {
auto instance = std::dynamic_pointer_cast<Derived>(state);
if (instance == nullptr) {
throw jsi::JSError(rt, "Invalid boxed native object state");
}
// The constructor makes `instanceof` work on this runtime too
// (a no-op once it is installed there).
Derived::installConstructor(rt);
// Unboxing hands back the wrapper cached for the target runtime,
// creating it on first use. Keep the runtime the object was
// originally created on: async native code (GPUDevice error/lost
// events, GPUAdapter::requestDevice) delivers into the creation
// runtime, and rebinding it to a worklet runtime would invoke
// main-runtime jsi::Functions on the wrong runtime and thread.
auto *originalRuntime = instance->getCreationRuntime();
auto value = Derived::create(rt, instance);
if (originalRuntime != nullptr) {
instance->setCreationRuntime(originalRuntime);
}
return value;
});
});

installPrototype(runtime);

// Once per runtime. installWebGPU() runs this at the top of every worklet
Expand Down Expand Up @@ -481,6 +456,41 @@ class NativeObject : public NativeObjectBase,
runtime.global().setProperty(runtime, Derived::CLASS_NAME, std::move(ctor));
}

/**
* Registers the reconstructor BoxedWebGPUObject::unbox() uses to rebuild
* this class's objects (prototype + native state) on another runtime.
* Once per process: the registry outlives every runtime.
*/
static void registerReconstructor() {
static std::once_flag registryFlag;
std::call_once(registryFlag, []() {
NativeObjectRegistry::getInstance().registerClass(
Derived::CLASS_NAME,
[](jsi::Runtime &rt,
std::shared_ptr<jsi::NativeState> state) -> jsi::Value {
auto instance = std::dynamic_pointer_cast<Derived>(state);
if (instance == nullptr) {
throw jsi::JSError(rt, "Invalid boxed native object state");
}
// The constructor makes `instanceof` work on this runtime too
// (a no-op once it is installed there).
Derived::installConstructor(rt);
// Unboxing hands back the wrapper cached for the target runtime,
// creating it on first use. Keep the runtime the object was
// originally created on: async native code (GPUDevice error/lost
// events, GPUAdapter::requestDevice) delivers into the creation
// runtime, and rebinding it to a worklet runtime would invoke
// main-runtime jsi::Functions on the wrong runtime and thread.
auto *originalRuntime = instance->getCreationRuntime();
auto value = Derived::create(rt, instance);
if (originalRuntime != nullptr) {
instance->setCreationRuntime(originalRuntime);
}
return value;
});
});
}

/**
* Returns the JS wrapper of `instance` on `runtime`, creating it on first
* use.
Expand Down
7 changes: 2 additions & 5 deletions packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "GPUTextureView.h"
#include "GPUUncapturedErrorEvent.h"
#include "GPUValidationError.h"
#include "ImageBitmap.h"
#include "VideoFrame.h"
#include "VideoPlayer.h"

Expand Down Expand Up @@ -78,11 +79,6 @@ RNWebGPUManager::RNWebGPUManager(
std::make_shared<RNWebGPU>(gpu, _platformContext, _jsCallInvoker);
_gpu = gpu->get();

// RNWebGPU needs its brand registered in NativeObjectRegistry so the boxing
// path can install the prototype on worklet runtimes. installConstructor
// does that registration but also sets globalThis.RNWebGPU = ctor, so we
// call it FIRST and then overwrite the global with the actual instance.
RNWebGPU::installConstructor(*_jsRuntime);
_jsRuntime->global().setProperty(*_jsRuntime, "RNWebGPU",
RNWebGPU::create(*_jsRuntime, rnWebGPU));

Expand Down Expand Up @@ -144,6 +140,7 @@ void RNWebGPUManager::installConstructors(jsi::Runtime &runtime) {
GPUSupportedLimits::installConstructor(runtime);
GPUTexture::installConstructor(runtime);
GPUTextureView::installConstructor(runtime);
ImageBitmap::installConstructor(runtime);
VideoFrame::installConstructor(runtime);
VideoPlayer::installConstructor(runtime);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/webgpu/cpp/rnwgpu/api/VideoFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ namespace jsi = facebook::jsi;
class VideoFrame : public NativeObject<VideoFrame> {
public:
// JS-facing brand. installConstructor() exposes this as
// globalThis[CLASS_NAME] and keys the NativeObjectRegistry / worklet
// serializer off it, so it must NOT be "VideoFrame": that would install our
// globalThis[CLASS_NAME], and the NativeObjectRegistry / worklet serializer
// are keyed off it, so it must NOT be "VideoFrame": that would install our
// constructor over the WebCodecs `VideoFrame` global and shadow it at
// runtime. "NativeVideoFrame" matches the public TypeScript type (see
// src/types.ts) and keeps the two namespaces distinct. The C++ class stays
Expand Down
74 changes: 74 additions & 0 deletions packages/webgpu/src/__tests__/WorkletSerialization.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import fs from "fs";
import path from "path";

import { client } from "./setup";

// A 32x32 PNG, decoded on the main runtime for the ImageBitmap round trip.
const assetPath = path.resolve(__dirname, "./assets/alpha-gradient.png");

// WebGPU objects are plain JS objects with jsi::NativeState and a per-runtime
// prototype, which worklets cannot copy by itself. registerWebGPUForReanimated
// registers a custom serializer whose native half is the
Expand Down Expand Up @@ -111,6 +117,74 @@ describe("Worklet serialization", () => {
});
});

it("round-trips an ImageBitmap through a worklet runtime", async () => {
// ImageBitmap was missing from RNWebGPUManager::installConstructors, so
// its brand was never registered and unboxing it on a worklet runtime
// threw "No native class registered for brand: ImageBitmap".
const pngData = Array.from(fs.readFileSync(assetPath));
const result = await client.eval<
{ pngData: number[] },
{
width: number;
height: number;
isImageBitmap: boolean;
sameObjectBack: boolean;
error?: string;
} | null
>(
({ workletSerialization, pngData: encodedPng }) => {
if (!workletSerialization) {
return null;
}
const bytes = new Uint8Array(encodedPng);
return createImageBitmap(bytes.buffer).then((bitmap) =>
workletSerialization.roundTripImageBitmap(bitmap),
);
},
{ pngData },
);
if (result === null) {
return;
}
expect(result).toEqual({
width: 32,
height: 32,
isImageBitmap: true,
sameObjectBack: true,
});
});

it("round-trips a Canvas through a worklet runtime", async () => {
// Classes used to be registered for unboxing only by installConstructor,
// and Canvas, which has no global constructor, never was. The native
// surface of an unknown context is a 0x0 Canvas.
const result = await client.eval<
Record<string, unknown>,
{
width: number;
sameObjectBack: boolean;
widthOnMain: number;
error?: string;
} | null
>(({ workletSerialization }) => {
if (!workletSerialization) {
return null;
}
const canvas = RNWebGPU.getNativeSurface(-1);
return workletSerialization
.roundTripCanvas(canvas)
.then((r) => ({ ...r, widthOnMain: canvas.width }));
});
if (result === null) {
return;
}
expect(result).toEqual({
width: 0,
sameObjectBack: true,
widthOnMain: 1,
});
});

it("installs every class constructor on a worklet runtime", async () => {
// Issue #564: three.js checks `pass instanceof GPURenderBundleEncoder` on
// every render. Unboxing installs only the classes of the objects that
Expand Down
12 changes: 12 additions & 0 deletions packages/webgpu/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ interface GPUTestingContext {
sameObjectBack: boolean;
error?: string;
}>;
roundTripImageBitmap: (bitmap: ImageBitmap) => Promise<{
width: number;
height: number;
isImageBitmap: boolean;
sameObjectBack: boolean;
error?: string;
}>;
roundTripCanvas: (canvas: { width: number }) => Promise<{
width: number;
sameObjectBack: boolean;
error?: string;
}>;
captureForeign: (
value: object,
) => Promise<{ threw: boolean; message: string; typeOnWorklet: string }>;
Expand Down
Loading