diff --git a/apps/docs/content/api/canvas.mdx b/apps/docs/content/api/canvas.mdx index dc34e4594..46df2fabe 100644 --- a/apps/docs/content/api/canvas.mdx +++ b/apps/docs/content/api/canvas.mdx @@ -154,7 +154,7 @@ On Android, `Canvas` can be backed by one of three native views, with different | Opaque by default | no | yes | never | | Status | default | opt-in | experimental, opt-in | -**`TextureView`** is the default: a regular view, so transforms, clipping, alpha and z-order all apply, and the canvas can be composited over other React Native views. The cost is an extra copy, typically a frame of latency, and occasional stalls during some view animations. +**`TextureView`** is the default: a regular view, so transforms, clipping, alpha and z-order all apply, and the canvas can be composited over other React Native views. The cost is an extra copy, typically a frame of latency, and occasional stalls during some view animations. A frame presented on the UI thread, from a worklet, reaches the `TextureView` behind the draw its window has pending, so that the window draws every frame, except on Android 11. **`SurfaceView`** is the fastest option. It is not regular view content, though: parent transforms, clipping, rounded corners and z-ordering with sibling views do not apply to it. Use it for a canvas that is a plain rectangle: a full-screen scene or a game. It is opaque unless asked otherwise: a translucent `SurfaceView` blends against the window background (usually black) instead of the React Native views beneath it, which are punched out. Pair it with `zOrderOnTop` to composite over React Native content, and note that `zOrderOnTop` draws above all React Native views in the window, including navigation screens, regardless of `zIndex`. diff --git a/apps/example/src/Tests.tsx b/apps/example/src/Tests.tsx index e62dfd551..2503e85f4 100644 --- a/apps/example/src/Tests.tsx +++ b/apps/example/src/Tests.tsx @@ -7,7 +7,9 @@ import { PixelRatio, findNodeHandle, } from "react-native"; +import type { CanvasRef } from "react-native-webgpu"; import { + Canvas, GPUOffscreenCanvas, // The flags as the package exports them, not the globals of the same name // (see the `exportedFlags` spec in Constants.spec.ts). @@ -25,6 +27,7 @@ import { redFragWGSL, triangleVertWGSL } from "./Triangle/triangle"; import type { AssetProps } from "./components/useAssets"; import { Texture } from "./components/Texture"; import { workletDeviceStress } from "./components/workletDeviceStress"; +import { workletPresentFrames } from "./components/workletPresentFrames"; import { workletSerialization } from "./components/workletSerialization"; export const CI = process.env.CI === "true"; @@ -33,6 +36,7 @@ const { width } = Dimensions.get("window"); const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); const PROBE_SIZE = 64; const QUADRANT = PROBE_SIZE / 2; +const TEXTURE_CANVAS_SIZE = 32; export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { const [texture, setTexture] = useState(null); @@ -42,6 +46,10 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { // A fixed native view the drawElementImageToTexture spec snapshots: four // 32x32pt solid quadrants (red, lime, blue, white), 64x64pt in total. const probeRef = useRef(null); + // A canvas backed by a TextureView on Android, for the TextureView + // present spec, and the view around it, which that spec snapshots. + const textureCanvasRef = useRef(null); + const textureCanvasViewRef = useRef(null); useEffect(() => { let cancelled = false; (async () => { @@ -129,8 +137,14 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { probe: findNodeHandle(probeRef.current), probeSize: PROBE_SIZE, pixelRatio: PixelRatio.get(), + textureCanvas: () => textureCanvasRef.current, + textureCanvasView: findNodeHandle( + textureCanvasViewRef.current, + ), + textureCanvasSize: TEXTURE_CANVAS_SIZE, }, workletDeviceStress, + workletPresentFrames, workletSerialization, exportedFlags: { GPUBufferUsage: exportedBufferUsage, @@ -229,6 +243,17 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => { }} /> + + + => + new Promise((resolve) => { + scheduleOnUI(() => { + "worklet"; + const present = (color: GPUColor) => { + const encoder = device.createCommandEncoder(); + encoder + .beginRenderPass({ + colorAttachments: [ + { + view: context.getCurrentTexture().createView(), + clearValue: color, + loadOp: "clear", + storeOp: "store", + }, + ], + }) + .end(); + device.queue.submit([encoder.finish()]); + context.present(); + }; + let next = 0; + const step = () => { + if (next === 0) { + present(colors[next++]); + } + if (next < colors.length) { + present(colors[next++]); + requestAnimationFrame(step); + return; + } + requestAnimationFrame(() => + requestAnimationFrame(() => scheduleOnRN(resolve)), + ); + }; + requestAnimationFrame(step); + }); + }); diff --git a/packages/webgpu/android/cpp/cpp-adapter.cpp b/packages/webgpu/android/cpp/cpp-adapter.cpp index b3768488c..fb9378303 100644 --- a/packages/webgpu/android/cpp/cpp-adapter.cpp +++ b/packages/webgpu/android/cpp/cpp-adapter.cpp @@ -165,9 +165,11 @@ namespace { // Weak handle on a Java view and one of its no-argument methods, for native // threads to call into it: the fence waiter wakes a WebGPUHardwareBufferView // when a frame is ready, the presenting thread tells a SurfaceView it is -// animating (AnimationReporter). Holding the view weakly means a view that RN -// has dropped is not kept alive by a SurfaceInfo that outlives it (the -// registry keeps the SurfaceInfo until onDropViewInstance). +// animating (AnimationReporter), and a TextureView canvas presenting on the +// main thread posts its present behind the window's pending draw. Holding the +// view weakly means a view that RN has dropped is not kept alive by a +// SurfaceInfo that outlives it (the registry keeps the SurfaceInfo until +// onDropViewInstance). struct WeakViewCall { jweak view = nullptr; jmethodID method = nullptr; @@ -188,22 +190,25 @@ struct WeakViewCall { } } - // Called on a native thread, outside the SurfaceInfo locks. - void operator()() const { + // Returns whether the view was there to take the call. Called on a native + // thread, outside the SurfaceInfo locks. + bool operator()() const { facebook::jni::ThreadScope scope; JNIEnv *env = facebook::jni::Environment::current(); if (env == nullptr || method == nullptr) { - return; + return false; } jobject local = env->NewLocalRef(view); if (local == nullptr) { - return; // the view was garbage collected + return false; // the view was garbage collected } env->CallVoidMethod(local, method); - if (env->ExceptionCheck()) { + const bool called = !env->ExceptionCheck(); + if (!called) { env->ExceptionClear(); } env->DeleteLocalRef(local); + return called; } }; @@ -284,7 +289,8 @@ void setAnimationReporter(int contextId, extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate( JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width, - jfloat height, jboolean reportAnimation) { + jfloat height, jboolean presentsBehindPendingDraw, + jboolean reportAnimation) { if (manager == nullptr) { return; } @@ -298,13 +304,21 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate( auto &runtime = rnwgpu::WebGPURuntime::shared(); auto gpu = runtime.instance(); auto surface = runtime.makeSurface(window); + rnwgpu::NativeSurfacePresentPoster presentPoster; + if (presentsBehindPendingDraw) { + auto postPresent = std::make_shared( + env, thiz, "postPresentBehindPendingDraw"); + presentPoster = [postPresent]() { return (*postPresent)(); }; + } // Find-or-create + attach runs atomically under the registry lock so a // concurrent destroyContext cannot orphan this surface. auto info = registry.attachSurface( contextId, gpu, static_cast(width), static_cast(height), window, - surface, [](void *nativeSurface) { + surface, + [](void *nativeSurface) { ANativeWindow_release(static_cast(nativeSurface)); - }); + }, + std::move(presentPoster)); // A SurfaceView asks to hear when it animates, for its refresh rate vote // (WebGPUView.onAnimating). if (reportAnimation) { @@ -322,6 +336,15 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate( manager->flushPendingSurfaceTransition(info); } +// The present WebGPUView.postPresentBehindPendingDraw posted (UI thread). +extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_presentPostedFrame( + JNIEnv *env, jobject thiz, jint contextId) { + auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); + if (auto info = registry.getSurfaceInfo(contextId)) { + info->presentPostedFrame(); + } +} + extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz, jint contextId) { diff --git a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java index 04663c300..ab7f4c4f1 100644 --- a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java +++ b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java @@ -2,6 +2,8 @@ import android.content.Context; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.Surface; import android.view.View; @@ -36,6 +38,10 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI { private View mView = null; @Nullable private BackingViewKind mKind = null; + // Native posts a frame presented on the main thread here, to present it + // behind the draw the window has pending (see SurfaceRegistry.h). + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final Runnable mPostedPresent = () -> presentPostedFrame(mContextId); // Only a SurfaceView votes: a TextureView or HardwareBufferView is // composited into the window, which votes for it. private final SurfaceFrameRateVote mFrameRateVote = new SurfaceFrameRateVote( @@ -194,7 +200,15 @@ public void surfaceCreated(Surface surface) { if (votesFrameRate) { mFrameRateVote.attach(surface); } - onSurfaceCreate(surface, mContextId, width, height, votesFrameRate); + onSurfaceCreate(surface, mContextId, width, height, presentsBehindPendingDraw(), votesFrameRate); + } + + // A TextureView shows a new frame only when its window draws. Android 11's + // Vulkan loader gives a TextureView's buffer queue a 4.3 s dequeue timeout, + // and a present held behind the draw there stretches those waits into + // freezes long enough for an ANR. + private boolean presentsBehindPendingDraw() { + return mKind == BackingViewKind.TEXTURE_VIEW && Build.VERSION.SDK_INT != Build.VERSION_CODES.R; } @Override @@ -226,6 +240,12 @@ public int getContextId() { return mContextId; } + /** Called by native on the main thread, see mPostedPresent. */ + @DoNotStrip + private void postPresentBehindPendingDraw() { + mMainHandler.post(mPostedPresent); + } + /** * Called from WebGPUViewManager.onDropViewInstance when React removes this * view: the view dies with its Canvas, so it retires the registry entry. @@ -235,14 +255,17 @@ public void destroy() { onViewDestroyed(mContextId); } - // reportAnimation asks native to call onAnimating() when frames start being - // presented to this surface back to back. + // presentsBehindPendingDraw has native post a present made on the main + // thread through postPresentBehindPendingDraw(); reportAnimation asks native + // to call onAnimating() when frames start being presented to this surface + // back to back. @DoNotStrip private native void onSurfaceCreate( Surface surface, int contextId, float width, float height, + boolean presentsBehindPendingDraw, boolean reportAnimation ); @@ -254,6 +277,9 @@ private native void onSurfaceChanged( float height ); + @DoNotStrip + private native void presentPostedFrame(int contextId); + @DoNotStrip private native void switchToOffscreenSurface(int contextId); diff --git a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h index e734e65f6..36234168e 100644 --- a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h +++ b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h @@ -58,6 +58,13 @@ struct Size { // CAMetalLayer on Apple platforms). May run on any thread. using NativeSurfaceReleaser = std::function; +// Installed with a surface whose window shows a new frame only when the +// window draws (Android's TextureView): has the main looper call +// SurfaceInfo::presentPostedFrame() behind the draw the window has pending. +// Returns false when it could not post. Called on the main thread, outside +// SurfaceInfo's lock. +using NativeSurfacePresentPoster = std::function; + #if defined(__ANDROID__) // --- AHB-pool presentation mode (WebGPUNativeBufferView) // ----------------------------- @@ -184,20 +191,24 @@ class SurfaceInfo { // RNWebGPUManager::flushPendingSurfaceTransition so contexts that are not // currently rendering also pick it up. void attachSurface(void *nativeSurface, wgpu::Surface surface, - NativeSurfaceReleaser releaser) { + NativeSurfaceReleaser releaser, + NativeSurfacePresentPoster presentPoster = nullptr) { void *replacedSurface = nullptr; NativeSurfaceReleaser replacedReleaser; + NativeSurfacePresentPoster replacedPresentPoster; { std::unique_lock lock(_mutex); if (_hasPendingAttach) { // Replaced before it was ever adopted. replacedSurface = _pendingNativeSurface; replacedReleaser = std::move(_pendingReleaser); + replacedPresentPoster = std::move(_pendingPresentPoster); } _hasPendingAttach = true; _pendingNativeSurface = nativeSurface; _pendingSurface = std::move(surface); _pendingReleaser = std::move(releaser); + _pendingPresentPoster = std::move(presentPoster); } if (replacedReleaser && replacedSurface) { replacedReleaser(replacedSurface); @@ -241,6 +252,7 @@ class SurfaceInfo { wgpu::Device device = nullptr; void *replacedSurface = nullptr; NativeSurfaceReleaser replacedReleaser; + NativeSurfacePresentPoster replacedPresentPoster; { std::unique_lock lock(_mutex); if (supersedeInFlightFrame) { @@ -255,6 +267,10 @@ class SurfaceInfo { // can be null if surface creation failed). replacedSurface = _nativeSurface; replacedReleaser = std::move(_releaser); + replacedPresentPoster = std::move(_presentPoster); + // A frame waiting for its posted present belongs to the replaced + // surface. + _presentPending = false; _surface = std::move(_pendingSurface); #if defined(__ANDROID__) // DAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASE @@ -262,6 +278,7 @@ class SurfaceInfo { #endif _nativeSurface = _pendingNativeSurface; _releaser = std::move(_pendingReleaser); + _presentPoster = std::move(_pendingPresentPoster); _hasPendingAttach = false; _pendingNativeSurface = nullptr; _frameEpoch++; @@ -319,6 +336,7 @@ class SurfaceInfo { std::vector viewFormats) { applyPendingAttach(/* supersedeInFlightFrame = */ true); std::unique_lock lock(_mutex); + presentPendingFrameLocked(); _viewFormats = std::move(viewFormats); _config = newConfig; // The caller's viewFormats storage dies with the call; point the stored @@ -355,6 +373,7 @@ class SurfaceInfo { if (_config.device == nullptr) { return; } + presentPendingFrameLocked(); _config.width = std::max(1, newWidth); _config.height = std::max(1, newHeight); _texture = nullptr; @@ -364,6 +383,7 @@ class SurfaceInfo { void unconfigure() { std::unique_lock lock(_mutex); + presentPendingFrameLocked(); if (_surface) { _surface.Unconfigure(); } @@ -490,6 +510,7 @@ class SurfaceInfo { "[WebGPU] getCurrentTexture() called on a canvas context that is " "not configured; call context.configure() first"); } + presentPendingFrameLocked(); _frameInFlight = true; _acquiredFromSurface = false; _frameEpoch++; @@ -525,10 +546,12 @@ class SurfaceInfo { // Present the current frame. Runs synchronously on the thread that did // getCurrentTexture/submit (main JS, Reanimated UI, or a worklet runtime), - // preserving Dawn surface thread-affinity. Frames whose texture was not - // acquired from the attached surface (offscreen, detached mid-frame, or - // acquire failure) are dropped. This is also the end-of-frame boundary: it - // adopts a surface that attached while the frame was in flight. + // preserving Dawn surface thread-affinity, except on a surface with a + // present poster (see presentsBehindPendingDrawLocked). Frames whose + // texture was not acquired from the attached surface (offscreen, detached + // mid-frame, or acquire failure) are dropped. This is also the + // end-of-frame boundary: it adopts a surface that attached while the frame + // was in flight. void presentFrame() { #if defined(__ANDROID__) if (_poolMode.load()) { @@ -549,21 +572,33 @@ class SurfaceInfo { dawn::native::metal::WaitForCommandsToBeScheduled(device.Get()); } #endif + NativeSurfacePresentPoster presentPoster; #if defined(__ANDROID__) std::function framePresented; #endif { std::unique_lock lock(_mutex); if (_surface && _acquiredFromSurface) { - _surface.Present(); + if (presentsBehindPendingDrawLocked()) { + _presentPending = true; + if (!_presentPosted) { + _presentPosted = true; + presentPoster = _presentPoster; + } + } else { + _surface.Present(); #if defined(__ANDROID__) - framePresented = _framePresented; + framePresented = _framePresented; #endif + } } _acquiredFromSurface = false; _frameInFlight = false; _frameEpoch++; } + if (presentPoster && !presentPoster()) { + presentPostedFrame(); + } #if defined(__ANDROID__) if (framePresented) { framePresented(); @@ -572,6 +607,15 @@ class SurfaceInfo { applyPendingAttach(); } + // Main thread, from the call a present poster posted: presents the frame + // presentFrame() left waiting, unless a frame boundary since has presented + // or dropped it. + void presentPostedFrame() { + std::unique_lock lock(_mutex); + _presentPosted = false; + presentPendingFrameLocked(); + } + NativeInfo getNativeInfo() { std::shared_lock lock(_mutex); // A surface that is still pending adoption is the one callers should see. @@ -718,6 +762,8 @@ class SurfaceInfo { void detach(bool createFallbackTexture) { void *releasedSurfaces[2] = {nullptr, nullptr}; NativeSurfaceReleaser releasers[2]; + // Destroyed outside the lock: they release JNI references. + NativeSurfacePresentPoster releasedPresentPosters[2]; #if defined(__ANDROID__) // Destroyed outside the lock: it releases a JNI reference. std::function releasedFrameReady; @@ -761,6 +807,7 @@ class SurfaceInfo { _pendingSurface = nullptr; releasedSurfaces[0] = _pendingNativeSurface; releasers[0] = std::move(_pendingReleaser); + releasedPresentPosters[0] = std::move(_pendingPresentPoster); _pendingNativeSurface = nullptr; } if (_surface) { @@ -773,14 +820,17 @@ class SurfaceInfo { _surfaceDevice = nullptr; #endif // The in-flight frame (if any) rendered into the destroyed surface; - // presentFrame() must not present it. + // presentFrame() must not present it, nor presentPostedFrame() one + // waiting for its posted present. _acquiredFromSurface = false; + _presentPending = false; } _frameEpoch++; // Window ownership is independent of the Dawn surface handle (which can // be null if surface creation failed): always return the window. releasedSurfaces[1] = _nativeSurface; releasers[1] = std::move(_releaser); + releasedPresentPosters[1] = std::move(_presentPoster); _nativeSurface = nullptr; } // Release outside the lock: the platform may do real work here. @@ -793,6 +843,29 @@ class SurfaceInfo { // All *Locked helpers below require _mutex to be held exclusively. + // A frame presented on the main thread can be inside a Choreographer + // frame, ahead of the window's draw, and a TextureView's buffer queue + // replaces a frame its window has not drawn yet: every other frame would + // never show. Such a frame is presented behind the draw the window has + // pending instead. Frames presented on another thread land between the + // main thread's frames and are presented straight away. + bool presentsBehindPendingDrawLocked() const { +#if defined(__ANDROID__) + return _presentPoster != nullptr && gettid() == getpid(); +#else + return false; +#endif + } + + // A frame waiting for its posted present is presented before the next + // frame boundary changes the surface, so no frame is acquired over it. + void presentPendingFrameLocked() { + if (_presentPending) { + _presentPending = false; + _surface.Present(); + } + } + wgpu::Texture createOffscreenTextureLocked() { wgpu::TextureDescriptor descriptor; // Union with the user's usage so offscreen frames stay compatible with @@ -916,6 +989,7 @@ class SurfaceInfo { _surfaceDevice = nullptr; // A frame acquired from the old surface can no longer be presented. _acquiredFromSurface = false; + _presentPending = false; _frameEpoch++; if (_nativeSurface) { wgpu::SurfaceSourceAndroidNativeWindow source; @@ -1229,9 +1303,16 @@ class SurfaceInfo { void *_pendingNativeSurface = nullptr; wgpu::Surface _pendingSurface = nullptr; NativeSurfaceReleaser _pendingReleaser; + NativeSurfacePresentPoster _pendingPresentPoster; // Frame state: set by getCurrentTexture, cleared by presentFrame. bool _frameInFlight = false; bool _acquiredFromSurface = false; + // The attached surface's present poster (null when it presents straight + // away), whether its last frame waits for the posted present, and whether + // a posted present is on its way. + NativeSurfacePresentPoster _presentPoster; + bool _presentPending = false; + bool _presentPosted = false; // Bumped on every acquire, present, configure/reconfigure/unconfigure, // adoption, and detach. The deferred blit-present in applyPendingAttach // revalidates against it so it never presents a texture that stopped being @@ -1322,14 +1403,15 @@ class SurfaceRegistry { // an entry that a concurrent destroyContext is erasing (it either marks the // entry attached before the check, or re-creates the entry after the // erase). Lock order is registry -> SurfaceInfo, matching every other path. - std::shared_ptr attachSurface(int id, wgpu::Instance gpu, - int width, int height, - void *nativeSurface, - wgpu::Surface surface, - NativeSurfaceReleaser releaser) { + std::shared_ptr + attachSurface(int id, wgpu::Instance gpu, int width, int height, + void *nativeSurface, wgpu::Surface surface, + NativeSurfaceReleaser releaser, + NativeSurfacePresentPoster presentPoster = nullptr) { std::unique_lock lock(_mutex); auto info = getSurfaceInfoOrCreateLocked(id, gpu, width, height); - info->attachSurface(nativeSurface, std::move(surface), std::move(releaser)); + info->attachSurface(nativeSurface, std::move(surface), std::move(releaser), + std::move(presentPoster)); return info; } diff --git a/packages/webgpu/src/__tests__/TextureViewPresent.spec.ts b/packages/webgpu/src/__tests__/TextureViewPresent.spec.ts new file mode 100644 index 000000000..c7cae3185 --- /dev/null +++ b/packages/webgpu/src/__tests__/TextureViewPresent.spec.ts @@ -0,0 +1,79 @@ +import { client } from "./setup"; + +// The harness mounts a 32x32pt canvas backed by a TextureView on Android +// (apps/example/src/Tests.tsx). A frame presented on the UI thread reaches it +// behind the window's pending draw; this checks that the window shows the last +// frame presented, also after a frame acquired while the previous one still +// waited for its present. + +type RGBA = [number, number, number, number]; + +const LIME: RGBA = [0, 255, 0, 255]; + +const expectColor = (actual: number[], expected: RGBA, tolerance = 3) => { + expected.forEach((channel, i) => { + expect(Math.abs(actual[i] - channel)).toBeLessThanOrEqual(tolerance); + }); +}; + +describe("TextureView canvas", () => { + it("shows the last frame a worklet presents on the UI thread", async () => { + if (client.OS !== "android") { + return; + } + const result = await client.eval( + ({ device, views, workletPresentFrames }) => { + const context = views?.textureCanvas()?.getContext("webgpu"); + const source = views?.textureCanvasView; + if (!views || !context || !source || !workletPresentFrames) { + throw new Error("The Tests screen has no TextureView canvas"); + } + context.configure({ + device, + format: navigator.gpu.getPreferredCanvasFormat(), + alphaMode: "opaque", + }); + const size = Math.round(views.textureCanvasSize * views.pixelRatio); + const texture = device.createTexture({ + size: [size, size], + format: "rgba8unorm", + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = Math.ceil((size * 4) / 256) * 256; + const output = device.createBuffer({ + size: bytesPerRow * size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const colors: GPUColor[] = [ + [1, 0, 0, 1], + [1, 1, 0, 1], + [0, 1, 1, 1], + [1, 0, 1, 1], + [0, 1, 0, 1], + ]; + return workletPresentFrames(device, context, colors) + .then(() => + device.queue.drawElementImageToTexture({ source }, { texture }), + ) + .then(() => { + const encoder = device.createCommandEncoder(); + encoder.copyTextureToBuffer( + { texture }, + { buffer: output, bytesPerRow }, + [size, size], + ); + device.queue.submit([encoder.finish()]); + return output.mapAsync(GPUMapMode.READ); + }) + .then(() => { + const data = new Uint8Array(output.getMappedRange()); + const centre = (size / 2) * bytesPerRow + (size / 2) * 4; + const pixel = Array.from(data.slice(centre, centre + 4)); + output.unmap(); + return pixel; + }); + }, + ); + expectColor(result, LIME); + }); +}); diff --git a/packages/webgpu/src/__tests__/setup.ts b/packages/webgpu/src/__tests__/setup.ts index c639e139f..ba6be30fc 100644 --- a/packages/webgpu/src/__tests__/setup.ts +++ b/packages/webgpu/src/__tests__/setup.ts @@ -11,6 +11,7 @@ import { mat4, vec3, mat3 } from "wgpu-matrix"; import type { Server, WebSocket } from "ws"; import type { Browser, Page } from "puppeteer"; +import type { CanvasRef, RNCanvasContext } from "../Canvas"; import type { GPUOffscreenCanvas } from "../Offscreen"; import { cubeVertexArray } from "./components/cube"; @@ -86,13 +87,26 @@ interface GPUTestingContext { GPUShaderStage: typeof GPUShaderStage; GPUTextureUsage: typeof GPUTextureUsage; }; + // Native app harness only: clears a canvas to each color in turn from + // Reanimated's UI thread, see + // apps/example/src/components/workletPresentFrames.ts. + workletPresentFrames?: ( + device: GPUDevice, + context: RNCanvasContext, + colors: GPUColor[], + ) => Promise; // Native app harness only: a mounted probe view for the // drawElementImageToTexture spec (its React tag, its size in points and the - // device pixel ratio). See apps/example/src/Tests.tsx. + // device pixel ratio), and for the TextureView present spec a canvas + // backed by a TextureView on Android, the React tag of the view around it + // and its size in points. See apps/example/src/Tests.tsx. views?: { probe: number | null; probeSize: number; pixelRatio: number; + textureCanvas: () => CanvasRef | null; + textureCanvasView: number | null; + textureCanvasSize: number; }; mat4: typeof mat4; vec3: typeof vec3;