|
| 1 | +import React, { useEffect, useRef } from "react"; |
| 2 | +import { StyleSheet, Text, View } from "react-native"; |
| 3 | +import type { CanvasRef } from "react-native-wgpu"; |
| 4 | +import { Canvas } from "react-native-wgpu"; |
| 5 | + |
| 6 | +type Mode = "combined" | "split"; |
| 7 | + |
| 8 | +const runPair = ( |
| 9 | + device: GPUDevice, |
| 10 | + contextA: GPUCanvasContext, |
| 11 | + contextB: GPUCanvasContext, |
| 12 | + format: GPUTextureFormat, |
| 13 | + mode: Mode, |
| 14 | + shouldStop: () => boolean, |
| 15 | +) => { |
| 16 | + contextA.configure({ device, format, alphaMode: "premultiplied" }); |
| 17 | + contextB.configure({ device, format, alphaMode: "premultiplied" }); |
| 18 | + |
| 19 | + const frame = () => { |
| 20 | + if (shouldStop()) { |
| 21 | + return; |
| 22 | + } |
| 23 | + |
| 24 | + const textureA = contextA.getCurrentTexture(); |
| 25 | + const textureB = contextB.getCurrentTexture(); |
| 26 | + |
| 27 | + const time = Date.now() / 1000; |
| 28 | + const r = (Math.sin(time * 2.0) + 1) / 2; |
| 29 | + const g = (Math.sin(time * 1.5 + Math.PI / 3) + 1) / 2; |
| 30 | + const b = (Math.sin(time * 1.0 + Math.PI / 2) + 1) / 2; |
| 31 | + |
| 32 | + const drawClear = ( |
| 33 | + encoder: GPUCommandEncoder, |
| 34 | + view: GPUTextureView, |
| 35 | + color: GPUColor, |
| 36 | + ) => { |
| 37 | + const pass = encoder.beginRenderPass({ |
| 38 | + colorAttachments: [ |
| 39 | + { |
| 40 | + view, |
| 41 | + clearValue: color, |
| 42 | + loadOp: "clear", |
| 43 | + storeOp: "store", |
| 44 | + }, |
| 45 | + ], |
| 46 | + }); |
| 47 | + pass.end(); |
| 48 | + }; |
| 49 | + |
| 50 | + if (mode === "combined") { |
| 51 | + // One encoder, two passes targeting two different surfaces, one |
| 52 | + // command buffer, one submit. Tracks that beginRenderPass accumulates |
| 53 | + // every color-attachment surface into the encoder's presentable set. |
| 54 | + const encoder = device.createCommandEncoder(); |
| 55 | + drawClear(encoder, textureA.createView(), [r, g, b, 1]); |
| 56 | + drawClear(encoder, textureB.createView(), [1 - r, 1 - g, 1 - b, 1]); |
| 57 | + device.queue.submit([encoder.finish()]); |
| 58 | + } else { |
| 59 | + // Two encoders, two command buffers, one submit. Tracks that |
| 60 | + // queue.submit aggregates presentable surfaces across every command |
| 61 | + // buffer in the array. |
| 62 | + const encoderA = device.createCommandEncoder(); |
| 63 | + drawClear(encoderA, textureA.createView(), [r, g, b, 1]); |
| 64 | + const encoderB = device.createCommandEncoder(); |
| 65 | + drawClear(encoderB, textureB.createView(), [1 - r, 1 - g, 1 - b, 1]); |
| 66 | + device.queue.submit([encoderA.finish(), encoderB.finish()]); |
| 67 | + } |
| 68 | + |
| 69 | + requestAnimationFrame(frame); |
| 70 | + }; |
| 71 | + |
| 72 | + frame(); |
| 73 | +}; |
| 74 | + |
| 75 | +const Pair = ({ mode, label }: { mode: Mode; label: string }) => { |
| 76 | + const refA = useRef<CanvasRef>(null); |
| 77 | + const refB = useRef<CanvasRef>(null); |
| 78 | + useEffect(() => { |
| 79 | + let stopped = false; |
| 80 | + (async () => { |
| 81 | + const adapter = await navigator.gpu.requestAdapter(); |
| 82 | + if (!adapter) { |
| 83 | + return; |
| 84 | + } |
| 85 | + const device = await adapter.requestDevice(); |
| 86 | + const contextA = refA.current?.getContext("webgpu"); |
| 87 | + const contextB = refB.current?.getContext("webgpu"); |
| 88 | + if (!contextA || !contextB) { |
| 89 | + return; |
| 90 | + } |
| 91 | + const format = navigator.gpu.getPreferredCanvasFormat(); |
| 92 | + runPair(device, contextA, contextB, format, mode, () => stopped); |
| 93 | + })(); |
| 94 | + return () => { |
| 95 | + stopped = true; |
| 96 | + }; |
| 97 | + }, [mode]); |
| 98 | + return ( |
| 99 | + <View style={styles.pair}> |
| 100 | + <Text style={styles.label}>{label}</Text> |
| 101 | + <View style={styles.row}> |
| 102 | + <Canvas ref={refA} style={styles.canvas} /> |
| 103 | + <Canvas ref={refB} style={styles.canvas} /> |
| 104 | + </View> |
| 105 | + </View> |
| 106 | + ); |
| 107 | +}; |
| 108 | + |
| 109 | +export const MultiCanvasSubmit = () => { |
| 110 | + return ( |
| 111 | + <View style={styles.container}> |
| 112 | + <Text style={styles.intro}> |
| 113 | + Each row drives two canvases that render inverted hues from a single |
| 114 | + submit. If the presentable-surface tracking is broken, one of the two |
| 115 | + canvases will stop updating (no display-link tick will present it). |
| 116 | + </Text> |
| 117 | + <Pair |
| 118 | + mode="combined" |
| 119 | + label="One encoder, two passes, one command buffer" |
| 120 | + /> |
| 121 | + <Pair |
| 122 | + mode="split" |
| 123 | + label="Two encoders, two command buffers, one submit" |
| 124 | + /> |
| 125 | + </View> |
| 126 | + ); |
| 127 | +}; |
| 128 | + |
| 129 | +const styles = StyleSheet.create({ |
| 130 | + container: { |
| 131 | + flex: 1, |
| 132 | + backgroundColor: "#111", |
| 133 | + padding: 12, |
| 134 | + }, |
| 135 | + intro: { |
| 136 | + color: "#f5f5f5", |
| 137 | + fontSize: 13, |
| 138 | + lineHeight: 18, |
| 139 | + marginBottom: 12, |
| 140 | + }, |
| 141 | + pair: { |
| 142 | + flex: 1, |
| 143 | + marginBottom: 12, |
| 144 | + }, |
| 145 | + label: { |
| 146 | + color: "#f5f5f5", |
| 147 | + fontSize: 13, |
| 148 | + marginBottom: 6, |
| 149 | + }, |
| 150 | + row: { |
| 151 | + flex: 1, |
| 152 | + flexDirection: "row", |
| 153 | + }, |
| 154 | + canvas: { |
| 155 | + flex: 1, |
| 156 | + marginRight: 6, |
| 157 | + }, |
| 158 | +}); |
0 commit comments