|
1 | | -import { useEffect, useMemo, useRef, useState } from "react"; |
2 | | -import { Button, PixelRatio, StyleSheet, Text, View } from "react-native"; |
3 | | -import type { CanvasRef } from "react-native-wgpu"; |
4 | | -import { Canvas, useDevice } from "react-native-wgpu"; |
5 | | -import * as d from "typegpu/data"; |
6 | | -import tgpu, { type TgpuBindGroup, type TgpuBuffer } from "typegpu"; |
7 | | - |
8 | | -import { vertWGSL, fragWGSL } from "./gradientWgsl"; |
9 | | - |
10 | | -const Span = d.struct({ |
11 | | - x: d.u32, |
12 | | - y: d.u32, |
13 | | -}); |
14 | | - |
15 | | -const bindGroupLayout = tgpu.bindGroupLayout({ |
16 | | - span: { uniform: Span }, |
17 | | -}); |
18 | | - |
19 | | -interface RenderingState { |
20 | | - pipeline: GPURenderPipeline; |
21 | | - spanBuffer: TgpuBuffer<typeof Span>; |
22 | | - bindGroup: TgpuBindGroup<(typeof bindGroupLayout)["entries"]>; |
23 | | -} |
24 | | - |
25 | | -function useRoot() { |
26 | | - const { device } = useDevice(); |
27 | | - |
28 | | - return useMemo( |
29 | | - () => (device ? tgpu.initFromDevice({ device }) : null), |
30 | | - [device], |
31 | | - ); |
32 | | -} |
| 1 | +import { useMemo, useState } from "react"; |
| 2 | +import { Button, StyleSheet, Text, View } from "react-native"; |
| 3 | +import { Canvas } from "react-native-wgpu"; |
| 4 | +import { common, d, std } from "typegpu"; |
| 5 | +import { useConfigureContext, useFrame, useMirroredUniform, useRoot } from "@typegpu/react"; |
33 | 6 |
|
34 | 7 | export function GradientTiles() { |
35 | | - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); |
36 | | - const [state, setState] = useState<null | RenderingState>(null); |
37 | | - const [spanX, setSpanX] = useState(4); |
38 | | - const [spanY, setSpanY] = useState(4); |
39 | 8 | const root = useRoot(); |
40 | | - const { device = null } = root ?? {}; |
41 | | - const ref = useRef<CanvasRef>(null); |
42 | 9 |
|
43 | | - useEffect(() => { |
44 | | - if (!device || !root || state !== null) { |
45 | | - return; |
46 | | - } |
47 | | - const context = ref.current?.getContext("webgpu")!; |
48 | | - |
49 | | - const canvas = context.canvas as HTMLCanvasElement; |
50 | | - canvas.width = canvas.clientWidth * PixelRatio.get(); |
51 | | - canvas.height = canvas.clientHeight * PixelRatio.get(); |
52 | | - context.configure({ |
53 | | - device, |
54 | | - format: presentationFormat, |
55 | | - }); |
56 | | - |
57 | | - const spanBuffer = root |
58 | | - .createBuffer(Span, { x: 10, y: 10 }) |
59 | | - .$usage("uniform"); |
60 | | - |
61 | | - const shader = device.createShaderModule({ |
62 | | - code: tgpu.resolve({ |
63 | | - template: `${vertWGSL} ${fragWGSL}`, |
64 | | - externals: { |
65 | | - _EXT_: { |
66 | | - span: bindGroupLayout.bound.span, |
67 | | - }, |
68 | | - }, |
69 | | - }), |
70 | | - }); |
| 10 | + const [spanX, setSpanX] = useState(4); |
| 11 | + const [spanY, setSpanY] = useState(4); |
71 | 12 |
|
72 | | - const pipeline = device.createRenderPipeline({ |
73 | | - layout: device.createPipelineLayout({ |
74 | | - bindGroupLayouts: [root.unwrap(bindGroupLayout)], |
75 | | - }), |
76 | | - vertex: { |
77 | | - module: shader, |
| 13 | + // Mirroring React state on the GPU as a uniform |
| 14 | + const span = useMirroredUniform(d.vec2u, d.vec2u(spanX, spanY)); |
| 15 | + |
| 16 | + const pipeline = useMemo(() => { |
| 17 | + // Defining a full-screen shader |
| 18 | + return root.createRenderPipeline({ |
| 19 | + vertex: common.fullScreenTriangle, |
| 20 | + fragment: ({ uv }) => { |
| 21 | + "use gpu"; |
| 22 | + const red = std.floor(uv.x * d.f32(span.$.x)) / d.f32(span.$.x); |
| 23 | + const green = std.floor(uv.y * d.f32(span.$.y)) / d.f32(span.$.y); |
| 24 | + return d.vec4f(red, green, 0.5, 1.0); |
78 | 25 | }, |
79 | | - fragment: { |
80 | | - module: shader, |
81 | | - targets: [ |
82 | | - { |
83 | | - format: presentationFormat, |
84 | | - }, |
85 | | - ], |
86 | | - }, |
87 | | - primitive: { |
88 | | - topology: "triangle-strip", |
89 | | - }, |
90 | | - }); |
91 | | - |
92 | | - const bindGroup = root.createBindGroup(bindGroupLayout, { |
93 | | - span: spanBuffer, |
94 | 26 | }); |
| 27 | + }, [root, span]); |
95 | 28 |
|
96 | | - setState({ bindGroup, pipeline, spanBuffer }); |
97 | | - }, [ref, device, root, presentationFormat, state]); |
98 | | - |
99 | | - useEffect(() => { |
100 | | - if (!device || !root || !state) { |
101 | | - return; |
102 | | - } |
103 | | - |
104 | | - const { bindGroup, pipeline, spanBuffer } = state; |
105 | | - const context = ref.current?.getContext("webgpu")!; |
106 | | - const textureView = context.getCurrentTexture().createView(); |
107 | | - const renderPassDescriptor: GPURenderPassDescriptor = { |
108 | | - colorAttachments: [ |
109 | | - { |
110 | | - view: textureView, |
111 | | - clearValue: [0, 0, 0, 0], |
112 | | - loadOp: "clear", |
113 | | - storeOp: "store", |
114 | | - }, |
115 | | - ], |
116 | | - }; |
| 29 | + const { ref, ctxRef } = useConfigureContext(); |
117 | 30 |
|
118 | | - spanBuffer.write({ x: spanX, y: spanY }); |
| 31 | + useFrame(() => { |
| 32 | + const ctx = ctxRef.current; |
| 33 | + if (!ctx) return; |
119 | 34 |
|
120 | | - const commandEncoder = device.createCommandEncoder(); |
121 | | - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); |
122 | | - passEncoder.setPipeline(pipeline); |
123 | | - passEncoder.setBindGroup(0, root.unwrap(bindGroup)); |
124 | | - passEncoder.draw(4); |
125 | | - passEncoder.end(); |
| 35 | + // Drawing to the canvas each frame |
| 36 | + pipeline.withColorAttachment({ view: ctx }).draw(3); |
126 | 37 |
|
127 | | - device.queue.submit([commandEncoder.finish()]); |
128 | | - context.present(); |
129 | | - }, [ref, device, root, spanX, spanY, state]); |
| 38 | + ctx.present?.(); |
| 39 | + }); |
130 | 40 |
|
131 | 41 | return ( |
132 | 42 | <View style={style.container}> |
133 | 43 | <Canvas ref={ref} style={style.webgpu} transparent /> |
134 | 44 | <View style={style.controls}> |
135 | 45 | <View style={style.buttonRow}> |
136 | 46 | <Text style={style.spanText}>span x: </Text> |
137 | | - <Button |
138 | | - title="β" |
139 | | - onPress={() => setSpanX((prev) => Math.max(1, prev - 1))} |
140 | | - /> |
141 | | - <Button |
142 | | - title="β" |
143 | | - onPress={() => setSpanX((prev) => Math.min(prev + 1, 10))} |
144 | | - /> |
| 47 | + <Button title="β" onPress={() => setSpanX((prev) => Math.max(1, prev - 1))} /> |
| 48 | + <Button title="β" onPress={() => setSpanX((prev) => Math.min(prev + 1, 10))} /> |
145 | 49 | </View> |
146 | 50 |
|
147 | 51 | <View style={style.buttonRow}> |
148 | 52 | <Text style={style.spanText}>span y: </Text> |
149 | | - <Button |
150 | | - title="β" |
151 | | - onPress={() => setSpanY((prev) => Math.max(1, prev - 1))} |
152 | | - /> |
153 | | - <Button |
154 | | - title="β" |
155 | | - onPress={() => setSpanY((prev) => Math.min(prev + 1, 10))} |
156 | | - /> |
| 53 | + <Button title="β" onPress={() => setSpanY((prev) => Math.max(1, prev - 1))} /> |
| 54 | + <Button title="β" onPress={() => setSpanY((prev) => Math.min(prev + 1, 10))} /> |
157 | 55 | </View> |
158 | 56 | </View> |
159 | 57 | </View> |
|
0 commit comments