Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { ViewFormatsUseAfterFree } from "./Diagnostics/ViewFormatsUseAfterFree";
import { RenderAfterUnmount } from "./Diagnostics/RenderAfterUnmount";
import { BackgroundDetach } from "./Diagnostics/BackgroundDetach";
import { SurfaceChurn } from "./Diagnostics/SurfaceChurn";
import { TransparencyMode } from "./Diagnostics/TransparencyMode";
import { StorageBufferVertices } from "./StorageBufferVertices";
import { SharedTextureMemory } from "./SharedTextureMemory";
import { ImportExternalTexture } from "./ImportExternalTexture";
Expand Down Expand Up @@ -126,6 +127,7 @@ function App() {
/>
<Stack.Screen name="BackgroundDetach" component={BackgroundDetach} />
<Stack.Screen name="SurfaceChurn" component={SurfaceChurn} />
<Stack.Screen name="TransparencyMode" component={TransparencyMode} />
<Stack.Screen
name="StorageBufferVertices"
component={StorageBufferVertices}
Expand Down
4 changes: 4 additions & 0 deletions apps/example/src/Diagnostics/DiagnosticsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const tests = [
screen: "SurfaceChurn",
title: "⚠️ Surface Churn",
},
{
screen: "TransparencyMode",
title: "⚠️ Android Transparency Mode",
},
] as const;

export const DiagnosticsList = () => {
Expand Down
112 changes: 112 additions & 0 deletions apps/example/src/Diagnostics/TransparencyMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useEffect, useRef, useState } from "react";
import { Button, Pressable, StyleSheet, Text, View } from "react-native";
import type { CanvasRef } from "react-native-webgpu";
import { Canvas } from "react-native-webgpu";

type Mode = "default" | "texture" | "surface-overlay";

const ClearCanvas = ({ mode }: { mode: Mode }) => {
const ref = useRef<CanvasRef>(null);

useEffect(() => {
let live = true;
(async () => {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter!.requestDevice();
const context = ref.current?.getContext("webgpu");
if (!context || !live) {
return;
}
context.configure({
device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: "premultiplied",
});
const frame = () => {
if (!live) {
return;
}
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: [0.5, 0, 0, 0.5],
loadOp: "clear",
storeOp: "store",
},
],
});
pass.end();
device.queue.submit([encoder.finish()]);
context.present();
setTimeout(frame, 200);
};
frame();
})().catch((error) => {
console.error(`[transparency] setup failed: ${error}`);
});
return () => {
live = false;
};
}, [mode]);

return (
<Canvas
ref={ref}
transparent
{...(mode === "default" ? {} : { androidTransparencyMode: mode })}
style={styles.canvas}
/>
);
};

export const TransparencyMode = () => {
const [mode, setMode] = useState<Mode>("default");
const [taps, setTaps] = useState(0);

return (
<View style={styles.container}>
<Text style={styles.copy}>
Half-transparent red canvas over a blue backdrop. In &quot;texture&quot;
mode the yellow overlay below stays visible on top of the canvas. In
&quot;surface-overlay&quot; mode the canvas gets its own layer and
covers the overlay.
</Text>
<View style={styles.buttons}>
<Button title="default" onPress={() => setMode("default")} />
<Button title="texture" onPress={() => setMode("texture")} />
<Button
title="surface-overlay"
onPress={() => setMode("surface-overlay")}
/>
</View>
<Text style={styles.copy}>
mode: {mode} overlay taps: {taps}
</Text>
<View style={styles.stage}>
<ClearCanvas key={mode} mode={mode} />
<Pressable style={styles.overlay} onPress={() => setTaps((t) => t + 1)}>
<Text>overlay (tap me)</Text>
</Pressable>
</View>
</View>
);
};

const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 8, paddingTop: 64 },
copy: { fontSize: 13 },
buttons: { flexDirection: "row", gap: 12 },
stage: { flex: 1, backgroundColor: "blue" },
canvas: { flex: 1 },
overlay: {
position: "absolute",
left: 24,
right: 24,
bottom: 40,
padding: 12,
alignItems: "center",
backgroundColor: "yellow",
},
});
1 change: 1 addition & 0 deletions apps/example/src/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type Routes = {
RenderAfterUnmount: undefined;
BackgroundDetach: undefined;
SurfaceChurn: undefined;
TransparencyMode: undefined;
StorageBufferVertices: undefined;
SharedTextureMemory: undefined;
ImportExternalTexture: undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

Expand All @@ -13,8 +14,18 @@ public class WebGPUSurfaceView extends SurfaceView implements SurfaceHolder.Call
WebGPUAPI mApi;

public WebGPUSurfaceView(Context context, WebGPUAPI api) {
this(context, api, false);
}

public WebGPUSurfaceView(Context context, WebGPUAPI api, boolean transparent) {
super(context);
mApi = api;
if (transparent) {
// Own SurfaceFlinger layer, so frames skip the app's HWUI renderer. The
// cost is that this layer sits above every other view in the window.
setZOrderOnTop(true);
getHolder().setFormat(PixelFormat.TRANSLUCENT);
}
getHolder().addCallback(this);
}

Expand Down
45 changes: 30 additions & 15 deletions packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {

private int mContextId;
private boolean mTransparent = false;
private boolean mTransparentSurfaceLayer = false;
private WebGPUModule mModule;
private View mView = null;

Expand All @@ -32,23 +33,37 @@ public void setContextId(int contextId) {
}

public void setTransparent(boolean value) {
if (value == mTransparent && mView != null) {
return;
}
mTransparent = value;
rebuildView();
}

// "surface-overlay" trades z-ordering for a cheaper composition path; see
// the androidTransparencyMode prop on Canvas. Ignored when not transparent.
public void setTransparencyMode(String mode) {
boolean surfaceLayer = "surface-overlay".equals(mode);
if (surfaceLayer == mTransparentSurfaceLayer && mView != null) {
return;
}
mTransparentSurfaceLayer = surfaceLayer;
rebuildView();
}

private void rebuildView() {
Context ctx = getContext();
if (value != mTransparent || mView == null) {
if (mView != null) {
removeView(mView);
}
mTransparent = value;
if (mTransparent) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// mView = new WebGPUAHBView(ctx, this);
// } else {
mView = new WebGPUTextureView(ctx, this);
// }
} else {
mView = new WebGPUSurfaceView(ctx, this);
}
addView(mView);
if (mView != null) {
removeView(mView);
}
if (mTransparent) {
mView = mTransparentSurfaceLayer
? new WebGPUSurfaceView(ctx, this, true)
: new WebGPUTextureView(ctx, this);
} else {
mView = new WebGPUSurfaceView(ctx, this);
}
addView(mView);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public void setTransparent(WebGPUView view, boolean value) {
view.setTransparent(value);
}

@Override
@ReactProp(name = "androidTransparencyMode")
public void setAndroidTransparencyMode(WebGPUView view, String value) {
view.setTransparencyMode(value);
}

@Override
@ReactProp(name = "contextId")
public void setContextId(WebGPUView view, int value) {
Expand Down
14 changes: 13 additions & 1 deletion packages/webgpu/src/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,21 @@ export interface CanvasRef {

interface CanvasProps extends ViewProps {
transparent?: boolean;
// Android only, and ignored unless `transparent` is set. "texture"
// (default) draws through a TextureView and stacks like any other view.
// "surface-overlay" gives the canvas its own translucent SurfaceFlinger
// layer, which skips a composition pass but draws above every React Native
// view in the window, so nothing can be layered on top of it.
androidTransparencyMode?: "texture" | "surface-overlay";
ref?: React.Ref<CanvasRef>;
}

export const Canvas = ({ transparent, ref, ...props }: CanvasProps) => {
export const Canvas = ({
transparent,
androidTransparencyMode,
ref,
...props
}: CanvasProps) => {
const viewRef = useRef(null);
const [contextId, _] = useState(() => generateContextId());
// Retire the native registry entry for this contextId on unmount. When a
Expand Down Expand Up @@ -96,6 +107,7 @@ export const Canvas = ({ transparent, ref, ...props }: CanvasProps) => {
style={{ flex: 1 }}
contextId={contextId}
transparent={!!transparent}
androidTransparencyMode={androidTransparencyMode}
/>
</View>
);
Expand Down
9 changes: 8 additions & 1 deletion packages/webgpu/src/WebGPUViewNativeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { codegenNativeComponent } from "react-native";
import type { Int32 } from "react-native/Libraries/Types/CodegenTypes";
import type {
Int32,
WithDefault,
} from "react-native/Libraries/Types/CodegenTypes";
import type { ViewProps } from "react-native";

export interface NativeProps extends ViewProps {
contextId: Int32;
transparent: boolean;
androidTransparencyMode?: WithDefault<
"texture" | "surface-overlay",
"texture"
>;
}

// eslint-disable-next-line import/no-default-export
Expand Down
9 changes: 8 additions & 1 deletion packages/webgpu/src/WebGPUViewNativeComponent.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { contextIdToId } from "./utils";
export interface NativeProps extends ViewProps {
contextId: Int32;
transparent: boolean;
androidTransparencyMode?: "texture" | "surface-overlay";
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -54,7 +55,13 @@ function resizeCanvas(canvas: HTMLCanvasElement | null) {
export default function WebGPUViewNativeComponent(
props: NativeProps,
): React.JSX.Element {
const { contextId, style, transparent, ...rest } = props;
const {
contextId,
style,
transparent,
androidTransparencyMode: _androidTransparencyMode,
...rest
} = props;

const canvasElm = useRef<HTMLCanvasElement>();

Expand Down