Skip to content

romansource/shader-job

Repository files navigation

ShaderJob — GPU computations in C# lambdas

Run compute shaders without writing HLSL. ShaderJob turns your C# lambdas into editor-time generated compute shaders and gives you a tiny fluent API to dispatch them at runtime.

  • No HLSL or kernel boilerplate
  • No manual resource binding
  • Just write a lambda and call Run

Performs GPU calculations via editor-time generated compute shaders from provided C# lambdas.

Why ShaderJob?

  • Stay in C#: author kernels as ordinary lambdas
  • Zero boilerplate: automatic shader generation, binding, and dispatch
  • Strongly-typed: generics for inputs and binding safety
  • Scale easily: 1D, 2D, 3D dispatch with a simple For(...)
  • Unity-friendly: ships as a UPM package and uses Addressables for runtime delivery

Requirements

  • Unity 6000.0 or newer (that's what I used)
  • Addressables 2.7.3 (installed automatically as a dependency)
  • A platform that supports compute shaders

Installation

Option A — Add via Package Manager UI

  1. Open Window → Package Manager.
  2. Click the + button → Add package from git URL...
  3. Paste: https://github.com/romansource/shader-job.git?path=src

Option B — manifest.json Add this to your Packages/manifest.json:

{
  "dependencies": {
    "io.github.romansource.shaderjob": "https://github.com/romansource/shader-job.git?path=src"
  }
}

After installation:

  • Addressables will be added automatically. If you plan to make a Player build, open Window → Asset Management → Addressables → Groups and Build → New Build → Default Build Script to generate content.

Note: Rebuild when you change kernels/lambdas that affect generated assets. In Editor, Addressables can pull from the Asset Database directly. In a Player, assets must be packed into Addressables content, hence the build step.

Quick start

1D dispatch:

using UnityEngine;
using RomanSource.ShaderJob; 

public class Example1D : MonoBehaviour
{
  void Start()
  {
    var src = new int[20];
    var dst = new int[20];
    for (int i = 0; i < src.Length; i++) src[i] = i;

    // Dispatch 20 threads (x dimension)
    // .Run accepts some lambda args to lower GC pressure by using static delegates
    ShaderJob.For(20).Run(src, dst, (input, output, id) =>
    {
      // id.x is in [0..19]
      output[id.x] = input[id.x] * 2;
    });

    Debug.Log($"dst[5] = {dst[5]}"); // 10
  }
}

2D dispatch:

// 2D grid: (gx, gy)
ShaderJob.For(16, 9).Run(src, dst, (input, output, id) =>
{
  // id.x in [0..15], id.y in [0..8]
  int width = 16;
  int idx = id.y * width + id.x;
  output[idx] = input[idx] + id.y;
});

3D dispatch:

// 3D grid: (gx, gy, gz)
ShaderJob.For(4, 4, 8).Run(input, output, a, b, (source, dist, const1, const2, id) =>
{
  int temp = source[id.x];
  for(int i; i < 32; i++)
  {
    // id.z in [0..7]
    temp = ((temp ^ id.x) + (temp * const1) - const2 * id.z;
  }
  dist[id.x] = temp;
});

Notes:

  • The lambda’s last parameter is a Vector3Int id (thread coordinates).
  • Generic arguments to Run(...) are your inputs. Arrays, ints, buffers, etc. are supported through generated bindings.

How it works (in one minute)

  • At edit time, ShaderJob analyzes your lambdas and generates equivalent compute shaders and binding code.
  • At runtime, the correct shader and bindings are looked up and dispatched via a tiny fluent API.
  • Addressables deliver the generated shaders in both editor play mode and player builds.

You keep the ergonomics of C# while getting the performance of GPU compute.

Best practices

  • Keep kernels pure if possible (treat inputs as read-only and write only to outputs) for clarity and fewer side effects.
  • Use the appropriate For(x), For(x, y) or For(x, y, z) shape to match your data layout to thread IDs.
  • Build Addressables content before making a Player build, so the generated shaders are included.
  • If you rename files or move code around, do a domain reload/recompile (commonly on Ctrl+S) so generation stays in sync.
  • The primary performance bottleneck is the CPU-GPU data transfer overhead, which is roughly the same for any number of elements (dispatch groups internally). So don't be shy to use big For(…) loop numbers, in my case that was 50k.
  • Type Inference Limitation: Currently, the system has limited type inference capabilities. Please be explicit with type declarations, such as int[] source = new[] { 1, 2, 3 };

Troubleshooting

  • “Nothing happens / output unchanged”

    • Ensure your dispatch dimensions match your data size and indexing math.
    • Check logs for Addressables warnings and build the Addressables content if running a standalone Player.
  • “Compute shaders not supported”

    • Confirm your target platform/GPU supports compute and the graphics API is set accordingly in Project Settings.

Details

📋 Roadmap

📋 Changelog

📋 MIT License

Contact info

romansourcemail@gmail.com