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

PlatformCompute ShadersAsync Readback
Windows/macOS/LinuxYesYes
iOS/AndroidYes (most devices)Yes
WebGLNoNo
WebGPUYesYes

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-unity

Basic 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 groups

Dispatch

// 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:

  1. Create a compute shader that writes particle data
  2. Use a GraphicsBuffer shared between compute and VFX Graph
  3. Dispatch from JavaScript to update the buffer
  4. VFX Graph reads the buffer each frame
This enables JavaScript-controlled particle systems with GPU performance.

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): StructType

ComputeShader

shader.kernel(name: string): KernelBuilder
shader.readback<T>(bufferName: string): Promise<T>
shader.dispose(): void

KernelBuilder

.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 times

ComputeBuffer

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