GPU Compute
OneJS provides a bridge to Unity's compute shader API, enabling GPU-accelerated computations from JavaScript. The onejs-unity package offers a fluent TypeScript API for working with compute shaders.
Related: Zero-Allocation Interop (for per-frame dispatch optimization)
Platform Support
| Platform | Compute Shaders | Async Readback |
|---|---|---|
| Windows/macOS/Linux | Yes | Yes |
| iOS/Android | Yes (most devices) | Yes |
| WebGL | No | No |
| WebGPU | Yes | Yes |
Setup
1. Register Shaders in C#
Compute shaders must be registered before JavaScript can access them:
using OneJS.GPU;
public class MyShaderProvider : MonoBehaviour {
public ComputeShader particleShader;
void OnEnable() {
GPUBridge.Register("ParticleUpdate", particleShader);
}
void OnDisable() {
GPUBridge.Unregister("ParticleUpdate");
}
}2. Install onejs-unity
npm install onejs-unityBasic Usage
import { compute, Platform } from "onejs-unity"
// Check platform support
if (!Platform.supportsCompute) {
console.log("Compute shaders not supported")
return
}
// Load a registered shader
const shader = await compute.load("ParticleUpdate")
// Create a buffer with initial data
const positions = compute.buffer({
data: new Float32Array([0, 0, 0, 1, 1, 1])
})
// Dispatch a kernel with the fluent API
shader.kernel("CSMain")
.float("deltaTime", 0.016)
.float("speed", 5.0)
.buffer("positions", positions)
.dispatch(1)
// Read results back from GPU
const result = await positions.read()
console.log(result) // Float32Array with updated values
// Clean up
positions.dispose()
shader.dispose()Fluent API
The kernel builder provides a chainable API for setting uniforms and dispatching:
Scalar Uniforms
shader.kernel("CSMain")
.float("time", performance.now() / 1000)
.int("count", 1024)
.bool("enabled", true)Vector Uniforms
shader.kernel("CSMain")
.vec2("resolution", [1920, 1080])
.vec3("gravity", [0, -9.8, 0])
.vec4("color", [1, 0.5, 0.2, 1])Vectors accept arrays or objects with x, y, z, w properties.
Matrix Uniforms
const transform = new Float32Array(16)
// ... fill matrix data
shader.kernel("CSMain")
.matrix("worldMatrix", transform)Buffer Bindings
// Create buffers
const input = compute.buffer({ data: new Float32Array(1024) })
const output = compute.buffer({ count: 1024 })
// Bind to kernel
shader.kernel("CSMain")
.buffer("inputData", input)
.buffer("outputData", output)
.dispatch(16) // 16 thread groupsDispatch
// 1D dispatch
shader.kernel("CSMain").dispatch(64)
// 2D dispatch
shader.kernel("CSMain").dispatch(32, 32)
// 3D dispatch
shader.kernel("CSMain").dispatch(8, 8, 8)
// Multiple iterations
shader.kernel("Simulate").repeat(100)Buffers
Creating Buffers
// From existing data
const buffer = compute.buffer({
data: new Float32Array([1, 2, 3, 4])
})
// Empty buffer with count
const empty = compute.buffer({ count: 1024 })
// Typed buffers
const ints = compute.buffer({
data: new Int32Array([1, 2, 3])
})Writing Data
buffer.write(new Float32Array([5, 6, 7, 8]))Reading Data (Async)
GPU readback is asynchronous:
const result = await buffer.read()For non-blocking checks:
// Start readback
buffer.read().then(data => {
console.log("Readback complete:", data)
})
// Check if ready
if (buffer.readbackReady) {
const data = buffer.readbackResult
}Structured Buffers
Define struct types matching your HLSL:
// HLSL struct:
// struct Particle {
// float3 position;
// float3 velocity;
// float life;
// };
const ParticleType = compute.struct({
position: "float3",
velocity: "float3",
life: "float"
})
const particles = compute.buffer({
count: 1000,
type: ParticleType
})Compute Shader Example
HLSL compute shader (ParticleUpdate.compute):
#pragma kernel CSMain
RWStructuredBuffer<float3> positions;
float deltaTime;
float speed;
uint count;
[numthreads(64, 1, 1)]
void CSMain(uint id : SV_DispatchThreadID) {
if (id >= count) return;
positions[id] += float3(0, speed * deltaTime, 0);
}JavaScript usage:
const shader = await compute.load("ParticleUpdate")
const positions = compute.buffer({
data: new Float32Array([0,0,0, 1,0,0, 2,0,0]) // 3 particles
})
// Update loop
function update(dt: number) {
shader.kernel("CSMain")
.float("deltaTime", dt)
.float("speed", 2.0)
.int("count", 3)
.buffer("positions", positions)
.dispatch(1)
}VFX Graph Integration
Compute shaders can drive VFX Graph parameters by writing to shared buffers:
- Create a compute shader that writes particle data
- Use a GraphicsBuffer shared between compute and VFX Graph
- Dispatch from JavaScript to update the buffer
- VFX Graph reads the buffer each frame
Performance Tips
- Minimize readbacks: GPU-to-CPU transfers are slow
- Batch dispatches when possible
- Use appropriate thread group sizes (typically 64, 128, or 256)
- Keep buffers GPU-resident when data doesn't need CPU access
- Profile with Unity's Frame Debugger
- Use zero-allocation interop for per-frame dispatch calls
API Reference
Platform
Platform.supportsCompute // boolean
Platform.supportsAsyncReadback // boolean
Platform.maxComputeWorkGroupSize // [x, y, z]compute
compute.load(name: string): Promise<ComputeShader>
compute.buffer<T>(options: BufferOptions<T>): ComputeBuffer<T>
compute.struct(schema: StructSchema): StructTypeComputeShader
shader.kernel(name: string): KernelBuilder
shader.readback<T>(bufferName: string): Promise<T>
shader.dispose(): voidKernelBuilder
.float(name, value) // Set float uniform
.int(name, value) // Set int uniform
.bool(name, value) // Set bool uniform
.vec2(name, value) // Set Vector2
.vec3(name, value) // Set Vector3
.vec4(name, value) // Set Vector4
.matrix(name, value) // Set Matrix4x4
.buffer(name, data) // Bind buffer
.dispatch(x, y?, z?) // Execute kernel
.repeat(iterations) // Execute multiple timesComputeBuffer
buffer.count // Number of elements
buffer.stride // Bytes per element
buffer.write(data) // Upload data
buffer.read() // Async readback
buffer.readbackReady // Check if ready
buffer.readbackResult // Get cached result
buffer.dispose() // Release resources