MIT License NuGet

Home / Docs / Compute effects

Compute effects

Everything in the post chain — TAA, bloom, GTAO, DDGI, the sky LUTs — is a ComputeEffect, registered through the same public API you would use for your own. There is no privileged built-in path, which means an effect you write has exactly the capabilities the shipped ones have.

What an effect is

Four members. Name for diagnostics, Phase for when it runs, Initialize to create resources and Record to dispatch. An optional OnResize rebuilds anything sized to the window.

using Season.Rendering;

sealed class EdgeEffect : ComputeEffect
{
    public const string TextureName = "compute://edges";

    public override string Name => "edges";
    public override ComputePhase Phase => ComputePhase.AfterScene;

    ComputeKernel? _kernel;
    int _width, _height;

    public override bool Initialize(IGraphics graphics)
    {
        _width = graphics.Width;
        _height = graphics.Height;

        _kernel = graphics.CreateComputeKernel(new ComputeKernelDesc
        {
            Name = "edges",
            Source = new ShaderSourceSet { Hlsl = Hlsl, EntryPoint = "CSMain" },
            Bindings = /* ... */,
            WorkgroupX = 8, WorkgroupY = 8, WorkgroupZ = 1
        });

        if (_kernel == null) return false;
        return graphics.CreateComputeTexture(TextureName, _width, _height);
    }

    public override void Record(IGraphics graphics)
    {
        graphics.DispatchCompute(new ComputeDispatchArgs
        {
            Kernel = _kernel!,
            Resources = /* ... */,
            GroupsX = (_width + 7) / 8,
            GroupsY = (_height + 7) / 8,
            GroupsZ = 1
        });
    }
}
Returning false is the supported way to fail

Initialize returning false is not an error condition, it is the graceful-degradation path: the effect is dropped, nothing it created is left behind, and the frame runs without it. Use it for anything you discover at startup — no compute support on this device, no shader source for this backend, a mode the user has turned off. Every built-in effect does this, which is why the same application runs on a machine with no compute at all.

Kernels carry source for four backends

ShaderSourceSet has an Hlsl, Glsl, Msl and Wgsl slot plus an EntryPoint that defaults to CSMain. A null slot means that backend cannot run this kernel, and registration fails cleanly there rather than at draw time.

TypeWhat it is for
ComputeKernelDescName, source set, binding list, workgroup size. ValidateWorkgroupSize() catches the sizes no backend accepts.
ComputeBindingDescOne slot: its ComputeBindingType and, for storage textures, its ComputeStorageFormat.
ComputeResourceRefWhat you bind at dispatch — a texture name, a StorageBuffer or a render target. Implicit conversions mean you usually just pass the string.
ComputeDispatchArgsA ref struct: kernel, push constants, resource refs and group counts. Nothing to allocate per frame.

Workgroup size defaults to 8×8×1, which is the shape almost every full-screen kernel wants. Keep the value in the descriptor and the divisor in Record in agreement — nothing checks that for you, and the symptom is a missing strip of pixels along the right and bottom edges.

Registering, and why order matters

Register after base.Create(), when the graphics device exists. Effects in the same phase are recorded in registration order, so registration order is execution order.

void RegisterEffects()
{
    var g = Season.Basic.Graphics.Instance;

    // Returns false when the effect declined to initialize. Skip the
    // debug control that would display its output and carry on.
    if (FrameSchedule.RegisterCompute(g, new EdgeEffect()))
        AddControl(new Sprite2D { Name = EdgeEffect.TextureName, /* ... */ });
}
TAA must be registered before bloom

Both live in AfterScene, and bloom has to consume the resolved image. Registered the other way round, bloom amplifies the shimmer that TAA was about to remove, and the result is visible flicker on specular highlights rather than a subtle difference. The reference application registers plasma, scene copy, TAA, then bloom, in that order, with the reason in a comment.

UnregisterCompute releases everything the effect created; ResizeCompute is called for you on a window change and forwards to OnResize. You never call Execute — the frame does.

The two phases

PhaseWhenTypical use
FrameStartBefore the scene is drawn.Anything the scene will read: sky and transmittance LUTs, cloud noise, procedural textures, an SDF slice view.
AfterSceneScene colour, depth and velocity are available.Post: resolve, tone-mapping inputs, AO, bloom, debug visualizations.
Record may only dispatch

No enabling or disabling passes, no draw calls, no barriers. Synchronisation is handled inside the backend's DispatchCompute, which is what keeps one effect from corrupting another's resources and what lets four backends implement the same effect list. If a kernel needs the output of a previous kernel, dispatch them in sequence in the same Record — that is exactly what GTAO does for its two blur passes.

Outputs are named textures

An effect creates its storage textures under a compute:// name and publishes the interesting one on FrameSchedule so the rest of the frame can find it without knowing the type. GtaoEffect creates compute://gtao/ao and assigns it to FrameSchedule.AoTexture; the lighting pass reads that field.

EffectPhasePublishes
SkyAtmosphereEffectFrameStartSkyViewTexture, CloudNoiseTexture, AerialLutTexture
PlasmaEffectFrameStartcompute://plasma — the compute-baseline smoke test
Sdf3DViewEffectFrameStartcompute://sdf3dview
TaaEffectAfterScenecompute://taa0 / taa1 ping-pong, TaaActive
BloomEffectAfterSceneBloomTexture, plus its compute://bloom/ mip chain
GtaoEffectAfterSceneAoTexture
DdgiEffectAfterSceneProbe and SDF volumes
DepthView, VelocityView, SceneColorCopyAfterScenecompute://depthview, compute://velocityview, compute://scenecopy

FrameSchedule also exposes the pass outputs themselves — SceneColor, SceneDepth, SceneVelocity, ShadowMap, PostColor — and the flags that say whether the shadow and post passes ran at all. Read those in Initialize to decide whether your effect can work.

Debugging what you built

Because outputs are named textures and a Sprite2D draws a texture by name, putting any intermediate stage on screen is three lines. This is the entire implementation of the reference application's debug mode.

AddControl(new Sprite2D
{
    Name = Season.Rendering.Effects.GtaoEffect.TextureName,
    Color = Colors.White,
    PosX = 20, PosY = 580, Width = 240
});
Debug mode showing depth, velocity, ambient occlusion and probe views side by side Debug mode

Depth, motion vectors, GTAO, the bloom mip chain, the TAA history and the scene SDF are all reachable this way. The one exception is a 3D output: those use a compute3d:// prefix and a sprite cannot display a volume, so visualizing one needs a slice kernel — Sdf3DViewEffect is that kernel, and a reasonable template if you write a volumetric effect of your own.

Where to go from here

Post-processing and compute covers the frame graph these effects plug into and the per-backend support matrix. Global illumination and AO walks through the two largest examples.

That is the end of the guides. The source is the reference beyond this point, and the reference application in Apps/Engine is the worked example for every page here.