diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock
index 10e49aa98..ccdb59915 100644
--- a/apps/example/ios/Podfile.lock
+++ b/apps/example/ios/Podfile.lock
@@ -1865,7 +1865,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - react-native-wgpu (0.5.3):
+ - react-native-wgpu (0.5.4):
- boost
- DoubleConversion
- fast_float
@@ -2938,7 +2938,7 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
react-native-skia: 5bf2b2107cd7f2d806fd364f5e16b1c7554ed3cd
- react-native-wgpu: 27d4c1aaa89ba015e8c02d5dbf8abeaa83c4d523
+ react-native-wgpu: 5528610fabc9eb435d4ee578b4d6f7c1e133bf56
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index c1f2037d7..bd4120a78 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -35,6 +35,7 @@ import { ComputeToys } from "./ComputeToys";
import { Reanimated } from "./Reanimated";
import { AsyncStarvation } from "./Diagnostics/AsyncStarvation";
import { DeviceLostHang } from "./Diagnostics/DeviceLostHang";
+import { StorageBufferVertices } from "./StorageBufferVertices";
// The two lines below are needed by three.js
import "fast-text-encoding";
@@ -91,6 +92,10 @@ function App() {
+
diff --git a/apps/example/src/Home.tsx b/apps/example/src/Home.tsx
index 711978821..9272dfec9 100644
--- a/apps/example/src/Home.tsx
+++ b/apps/example/src/Home.tsx
@@ -123,6 +123,10 @@ export const examples = [
screen: "DeviceLostHang",
title: "⚠️ Device Lost Hang",
},
+ {
+ screen: "StorageBufferVertices",
+ title: "💾 Storage Buffer Vertices",
+ },
];
const styles = StyleSheet.create({
diff --git a/apps/example/src/Route.ts b/apps/example/src/Route.ts
index 67d82ad22..152923e1e 100644
--- a/apps/example/src/Route.ts
+++ b/apps/example/src/Route.ts
@@ -28,4 +28,5 @@ export type Routes = {
Reanimated: undefined;
AsyncStarvation: undefined;
DeviceLostHang: undefined;
+ StorageBufferVertices: undefined;
};
diff --git a/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx b/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx
new file mode 100644
index 000000000..907264638
--- /dev/null
+++ b/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx
@@ -0,0 +1,206 @@
+import React from "react";
+import { StyleSheet, View } from "react-native";
+import { Canvas } from "react-native-wgpu";
+
+import { useWebGPU } from "../components/useWebGPU";
+
+import { shaderCode } from "./shaders";
+
+const rand = (min?: number, max?: number) => {
+ if (min === undefined) {
+ min = 0;
+ max = 1;
+ } else if (max === undefined) {
+ max = min;
+ min = 0;
+ }
+ return min + Math.random() * (max - min);
+};
+
+function createCircleVertices({
+ radius = 1,
+ numSubdivisions = 24,
+ innerRadius = 0,
+ startAngle = 0,
+ endAngle = Math.PI * 2,
+} = {}) {
+ const numVertices = numSubdivisions * 3 * 2;
+ const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);
+
+ let offset = 0;
+ const addVertex = (x: number, y: number) => {
+ vertexData[offset++] = x;
+ vertexData[offset++] = y;
+ };
+
+ for (let i = 0; i < numSubdivisions; ++i) {
+ const angle1 =
+ startAngle + ((i + 0) * (endAngle - startAngle)) / numSubdivisions;
+ const angle2 =
+ startAngle + ((i + 1) * (endAngle - startAngle)) / numSubdivisions;
+
+ const c1 = Math.cos(angle1);
+ const s1 = Math.sin(angle1);
+ const c2 = Math.cos(angle2);
+ const s2 = Math.sin(angle2);
+
+ // first triangle
+ addVertex(c1 * radius, s1 * radius);
+ addVertex(c2 * radius, s2 * radius);
+ addVertex(c1 * innerRadius, s1 * innerRadius);
+
+ // second triangle
+ addVertex(c1 * innerRadius, s1 * innerRadius);
+ addVertex(c2 * radius, s2 * radius);
+ addVertex(c2 * innerRadius, s2 * innerRadius);
+ }
+
+ return {
+ vertexData,
+ numVertices,
+ };
+}
+
+export function StorageBufferVertices() {
+ const ref = useWebGPU(({ context, device, presentationFormat, canvas }) => {
+ const module = device.createShaderModule({
+ code: shaderCode,
+ });
+
+ const pipeline = device.createRenderPipeline({
+ label: "storage buffer vertices",
+ layout: "auto",
+ vertex: {
+ module,
+ },
+ fragment: {
+ module,
+ targets: [{ format: presentationFormat }],
+ },
+ });
+
+ const kNumObjects = 100;
+ const objectInfos: { scale: number }[] = [];
+
+ // create 2 storage buffers
+ const staticUnitSize =
+ 4 * 4 + // color is 4 32bit floats (4bytes each)
+ 2 * 4 + // offset is 2 32bit floats (4bytes each)
+ 2 * 4; // padding
+ const changingUnitSize = 2 * 4; // scale is 2 32bit floats (4bytes each)
+ const staticStorageBufferSize = staticUnitSize * kNumObjects;
+ const changingStorageBufferSize = changingUnitSize * kNumObjects;
+
+ const staticStorageBuffer = device.createBuffer({
+ label: "static storage for objects",
+ size: staticStorageBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+
+ const changingStorageBuffer = device.createBuffer({
+ label: "changing storage for objects",
+ size: changingStorageBufferSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+
+ // offsets to the various uniform values in float32 indices
+ const kColorOffset = 0;
+ const kOffsetOffset = 4;
+ const kScaleOffset = 0;
+
+ const staticStorageValues = new Float32Array(staticStorageBufferSize / 4);
+ for (let i = 0; i < kNumObjects; ++i) {
+ const staticOffset = i * (staticUnitSize / 4);
+
+ // These are only set once so set them now
+ staticStorageValues.set(
+ [rand(), rand(), rand(), 1],
+ staticOffset + kColorOffset,
+ ); // set the color
+ staticStorageValues.set(
+ [rand(-0.9, 0.9), rand(-0.9, 0.9)],
+ staticOffset + kOffsetOffset,
+ ); // set the offset
+
+ objectInfos.push({
+ scale: rand(0.2, 0.5),
+ });
+ }
+ device.queue.writeBuffer(staticStorageBuffer, 0, staticStorageValues);
+
+ // a typed array we can use to update the changingStorageBuffer
+ const storageValues = new Float32Array(changingStorageBufferSize / 4);
+
+ // setup a storage buffer with vertex data
+ const { vertexData, numVertices } = createCircleVertices({
+ radius: 0.5,
+ innerRadius: 0.25,
+ });
+ const vertexStorageBuffer = device.createBuffer({
+ label: "storage buffer vertices",
+ size: vertexData.byteLength,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+ });
+ device.queue.writeBuffer(vertexStorageBuffer, 0, vertexData);
+
+ const bindGroup = device.createBindGroup({
+ label: "bind group for objects",
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: staticStorageBuffer },
+ { binding: 1, resource: changingStorageBuffer },
+ { binding: 2, resource: vertexStorageBuffer },
+ ],
+ });
+
+ const renderPassDescriptor: GPURenderPassDescriptor = {
+ label: "our basic canvas renderPass",
+ colorAttachments: [
+ {
+ view: context.getCurrentTexture().createView(),
+ clearValue: [0.3, 0.3, 0.3, 1],
+ loadOp: "clear",
+ storeOp: "store",
+ },
+ ],
+ };
+
+ // Set the uniform values in our JavaScript side Float32Array
+ const aspect = canvas.width / canvas.height;
+
+ // set the scales for each object
+ objectInfos.forEach(({ scale }, ndx) => {
+ const offset = ndx * (changingUnitSize / 4);
+ storageValues.set([scale / aspect, scale], offset + kScaleOffset);
+ });
+ // upload all scales at once
+ device.queue.writeBuffer(changingStorageBuffer, 0, storageValues);
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(pipeline);
+ pass.setBindGroup(0, bindGroup);
+ pass.draw(numVertices, kNumObjects);
+ pass.end();
+
+ const commandBuffer = encoder.finish();
+ device.queue.submit([commandBuffer]);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (context as any).present();
+ });
+
+ return (
+
+
+
+ );
+}
+
+const style = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ webgpu: {
+ flex: 1,
+ },
+});
diff --git a/apps/example/src/StorageBufferVertices/index.ts b/apps/example/src/StorageBufferVertices/index.ts
new file mode 100644
index 000000000..7b62674c1
--- /dev/null
+++ b/apps/example/src/StorageBufferVertices/index.ts
@@ -0,0 +1 @@
+export * from "./StorageBufferVertices";
diff --git a/apps/example/src/StorageBufferVertices/shaders.ts b/apps/example/src/StorageBufferVertices/shaders.ts
new file mode 100644
index 000000000..e18ced264
--- /dev/null
+++ b/apps/example/src/StorageBufferVertices/shaders.ts
@@ -0,0 +1,41 @@
+export const shaderCode = /* wgsl */ `
+ struct OurStruct {
+ color: vec4f,
+ offset: vec2f,
+ };
+
+ struct OtherStruct {
+ scale: vec2f,
+ };
+
+ struct Vertex {
+ position: vec2f,
+ };
+
+ struct VSOutput {
+ @builtin(position) position: vec4f,
+ @location(0) color: vec4f,
+ };
+
+ @group(0) @binding(0) var ourStructs: array;
+ @group(0) @binding(1) var otherStructs: array;
+ @group(0) @binding(2) var pos: array;
+
+ @vertex fn vs(
+ @builtin(vertex_index) vertexIndex : u32,
+ @builtin(instance_index) instanceIndex: u32
+ ) -> VSOutput {
+ let otherStruct = otherStructs[instanceIndex];
+ let ourStruct = ourStructs[instanceIndex];
+
+ var vsOut: VSOutput;
+ vsOut.position = vec4f(
+ pos[vertexIndex].position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
+ vsOut.color = ourStruct.color;
+ return vsOut;
+ }
+
+ @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
+ return vsOut.color;
+ }
+`;
diff --git a/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h b/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h
index ef1570503..73e54dc43 100644
--- a/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h
+++ b/packages/webgpu/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h
@@ -46,6 +46,12 @@ template <> struct JSIConverter> {
} else if (obj.hasNativeState(runtime)) {
result->textureView =
obj.getNativeState(runtime);
+ } else if (obj.hasNativeState(runtime)) {
+ // Support passing GPUBuffer directly as resource (auto-wrap in
+ // GPUBufferBinding)
+ auto binding = std::make_shared();
+ binding->buffer = obj.getNativeState(runtime);
+ result->buffer = binding;
} else {
result->buffer = JSIConverter<
std::shared_ptr>::fromJSI(runtime,
diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json
index d174b8f56..1817c203d 100644
--- a/packages/webgpu/package.json
+++ b/packages/webgpu/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-wgpu",
- "version": "0.5.4",
+ "version": "0.5.5",
"description": "React Native WebGPU",
"main": "lib/commonjs/index",
"module": "lib/module/index",
diff --git a/packages/webgpu/src/__tests__/Device.spec.ts b/packages/webgpu/src/__tests__/Device.spec.ts
index ea6dd2758..d02dd57b7 100644
--- a/packages/webgpu/src/__tests__/Device.spec.ts
+++ b/packages/webgpu/src/__tests__/Device.spec.ts
@@ -1,5 +1,74 @@
import { client } from "./setup";
+describe("createBindGroup", () => {
+ it("should accept GPUBuffer directly as resource (without wrapper)", async () => {
+ const result = await client.eval(({ device }) => {
+ // Create a simple compute shader that uses a storage buffer
+ const module = device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var data: array;
+ @compute @workgroup_size(1)
+ fn main() {
+ let _ = data[0];
+ }
+ `,
+ });
+
+ const pipeline = device.createComputePipeline({
+ layout: "auto",
+ compute: { module },
+ });
+
+ const buffer = device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ // Pass GPUBuffer directly as resource (without { buffer: ... } wrapper)
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: buffer }],
+ });
+
+ return bindGroup !== null && bindGroup !== undefined;
+ });
+ expect(result).toBe(true);
+ });
+
+ it("should accept GPUBufferBinding object as resource (with wrapper)", async () => {
+ const result = await client.eval(({ device }) => {
+ const module = device.createShaderModule({
+ code: `
+ @group(0) @binding(0) var data: array;
+ @compute @workgroup_size(1)
+ fn main() {
+ let _ = data[0];
+ }
+ `,
+ });
+
+ const pipeline = device.createComputePipeline({
+ layout: "auto",
+ compute: { module },
+ });
+
+ const buffer = device.createBuffer({
+ size: 16,
+ usage: GPUBufferUsage.STORAGE,
+ });
+
+ // Pass GPUBufferBinding object as resource (with { buffer: ... } wrapper)
+ const bindGroup = device.createBindGroup({
+ layout: pipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer } }],
+ });
+
+ return bindGroup !== null && bindGroup !== undefined;
+ });
+ expect(result).toBe(true);
+ });
+});
+
describe("Device", () => {
it("request device (1)", async () => {
const result = await client.eval(({ gpu }) =>