using System.Collections.Generic;
using UnityEngine.Rendering;
// ----------------------------------------
// Resource handles (buffers here for simplicity)
// ----------------------------------------
public sealed class BufferHandle
public ComputeBuffer Buffer { get; private set; }
public int Count { get; private set; }
public int Stride { get; private set; }
public uint Version { get; private set; } = 1;
public string Name { get; }
public BufferHandle(string name) => Name = name;
public void Ensure(int count, int stride)
if (Buffer != null && Count == count && Stride == stride) return;
Buffer = new ComputeBuffer(count, stride, ComputeBufferType.Structured);
BumpVersion(); // layout changed => content effectively changed
public void BumpVersion() => Version++;
// ----------------------------------------
// CookContext: local scheduling scope
// ----------------------------------------
public sealed class CookContext : IDisposable
public readonly int FrameId;
public readonly uint ContextHash; // resolution/bounds/quality/simStep etc.
public readonly CommandBuffer Cmd;
private readonly HashSet<Node> _cooked = new HashSet<Node>();
private readonly Stack<Node> _stack = new Stack<Node>();
public CookContext(int frameId, uint contextHash, string cmdName = "CookContext")
ContextHash = contextHash;
Cmd = new CommandBuffer { name = $"{cmdName} {frameId}" };
public bool IsCooked(Node n) => _cooked.Contains(n);
public void MarkCooked(Node n) => _cooked.Add(n);
// build a helpful message
var path = string.Join(" -> ", _stack);
throw new InvalidOperationException($"Cycle detected while cooking {n.Name}. Stack: {path}");
Graphics.ExecuteCommandBuffer(Cmd);
// ----------------------------------------
// ----------------------------------------
public abstract class Node
public string Name { get; }
public uint ParamVersion { get; private set; } = 1;
// Cache: last signature that produced the current outputs
private ulong _lastSignature;
protected Node(string name) => Name = name;
public void TouchParams() => ParamVersion++;
public void EnsureCooked(CookContext ctx)
if (ctx.IsCooked(this)) return;
// 1) cycle detection scope
ulong sig = ComputeSignature(ctx);
// 3) if cache hit => skip recording work
if (sig == _lastSignature && IsOutputValid())
foreach (var dep in GetDependencies())
// 5) record work (this is the "cook")
// 6) publish cache signature
protected virtual bool IsOutputValid() => true;
protected abstract IEnumerable<Node> GetDependencies();
protected abstract ulong ComputeSignature(CookContext ctx);
protected abstract void Record(CookContext ctx);
// ----------------------------------------
// Graph: maps output names to nodes / resources
// ----------------------------------------
public sealed class ComputeGraph : IDisposable
private readonly Dictionary<string, Node> _outputToNode = new Dictionary<string, Node>();
public void RegisterOutput(string outputName, Node producer)
_outputToNode[outputName] = producer;
public T GetNode<T>(string outputName) where T : Node
return (T)_outputToNode[outputName];
public void EnsureOutputCooked(string outputName, CookContext ctx)
if (!_outputToNode.TryGetValue(outputName, out var node))
throw new KeyNotFoundException($"No node registered for output '{outputName}'.");
// Nodes own resources in this simple example, so nothing here.
// ----------------------------------------
// Example nodes: Fill -> Blur
// ----------------------------------------
public sealed class FillNode : Node
private readonly ComputeShader _cs;
private readonly int _kernel;
public readonly BufferHandle OutDensity = new BufferHandle("Density");
public FillNode(ComputeShader cs) : base("FillNode")
_kernel = cs.FindKernel("KFill");
public void SetParams(int count, float value)
if (_count != count) { _count = count; TouchParams(); }
if (!Mathf.Approximately(_value, value)) { _value = value; TouchParams(); }
protected override IEnumerable<Node> GetDependencies()
protected override ulong ComputeSignature(CookContext ctx)
// signature includes: context + params (via ParamVersion) + output layout (_count)
// ParamVersion already bumps on param change; include _count too for clarity.
return Hash64(ctx.ContextHash, ParamVersion, (uint)_count);
protected override void Record(CookContext ctx)
OutDensity.Ensure(_count, sizeof(float));
_cs.SetInt("_Count", _count);
_cs.SetFloat("_FillValue", _value);
_cs.SetBuffer(_kernel, "_Out", OutDensity.Buffer);
int groups = Mathf.CeilToInt(_count / 64f);
ctx.Cmd.DispatchCompute(_cs, _kernel, groups, 1, 1);
OutDensity.BumpVersion();
private static ulong Hash64(uint a, uint b, uint c)
x = (x * 1099511628211UL) ^ b;
x = (x * 1099511628211UL) ^ c;
public sealed class BlurNode : Node
private readonly ComputeShader _cs;
private readonly int _kernel;
private readonly FillNode _fill;
public readonly BufferHandle OutBlurred = new BufferHandle("BlurredDensity");
public BlurNode(ComputeShader cs, FillNode fill) : base("BlurNode")
_kernel = cs.FindKernel("KBlur1D");
public void SetParams(int count)
if (_count != count) { _count = count; TouchParams(); }
protected override IEnumerable<Node> GetDependencies()
protected override ulong ComputeSignature(CookContext ctx)
// depends on context, this node params, and input buffer version.
uint inVer = _fill.OutDensity.Version;
return Hash64(ctx.ContextHash, ParamVersion, inVer, (uint)_count);
protected override void Record(CookContext ctx)
// Ensure input is ready (it is, deps were cooked)
var inp = _fill.OutDensity;
OutBlurred.Ensure(_count, sizeof(float));
_cs.SetInt("_Count", _count);
_cs.SetBuffer(_kernel, "_In", inp.Buffer);
_cs.SetBuffer(_kernel, "_Out", OutBlurred.Buffer);
int groups = Mathf.CeilToInt(_count / 64f);
ctx.Cmd.DispatchCompute(_cs, _kernel, groups, 1, 1);
OutBlurred.BumpVersion();
private static ulong Hash64(uint a, uint b, uint c, uint d)
x = (x * 1099511628211UL) ^ b;
x = (x * 1099511628211UL) ^ c;
x = (x * 1099511628211UL) ^ d;