Skip to content
PRODUCT GITHUB  ↗

Compute Providers

Sometimes you want geometry that changes every frame — a rippling ocean, a GPU particle mesh, a marching-cubes surface — without ever touching it from the game thread. RMC’s compute path lets you allocate a section group’s GPU buffers with unordered-access (UAV) support and hand them to a compute shader that rewrites them inside the frame’s render graph.

This is a C++-only, render-thread workflow. There is no Blueprint surface. You’ll write RDG (Render Dependency Graph) passes, so you need RenderCore and RHI as module dependencies. The ready-made providers live in the RealtimeMeshCompute module.

Add what you need to your Build.cs:

PrivateDependencyModuleNames.AddRange(new string[] {
"RealtimeMeshComponent", // the compute registration API + compute-writable section groups
"RenderCore", // RDG / ENQUEUE_RENDER_COMMAND
"RHI",
"RealtimeMeshCompute", // shipped providers (FRealtimeMeshWaveProvider) — optional if you write your own
});

The compute registration API and FRealtimeMeshComputeContext live in the core RealtimeMeshComponent module (Compute/RealtimeMeshComputeProvider.h). You only need RealtimeMeshCompute for the shipped provider implementations, or you can write your own provider against the core interface plus RenderCore/RHI.

Two shipped example actors ground this page:

  • ARealtimeMeshExample_Simple_ComputeWave — Source/RealtimeMeshExamples/Private/Simple/RealtimeMeshExample_Simple_ComputeWave.cpp. A per-frame wave that rewrites a grid’s positions on the GPU.
  • ARealtimeMeshExample_Simple_ComputeIndirect — Source/RealtimeMeshExamples/Private/Simple/RealtimeMeshExample_Simple_ComputeIndirect.cpp. A GPU indirect-args buffer controlling how much of a mesh is drawn.

Step 1: create a compute-writable section group

Section titled “Step 1: create a compute-writable section group”

The buffers a compute pass writes must be allocated with UAV support up front. You request that with the bComputeWritable flag on FRealtimeMeshBufferSetConfig:

Source/RealtimeMeshComponent/Public/Interface/Core/RealtimeMeshBufferSetConfig.h
struct FRealtimeMeshBufferSetConfig
{
ERealtimeMeshSectionDrawType DrawType;
bool bComputeWritable; // allocate GPU buffers with UAV support for compute writes
FRealtimeMeshBufferSetConfig(ERealtimeMeshSectionDrawType InDrawType = ERealtimeMeshSectionDrawType::Static,
bool bInComputeWritable = false);
};

bComputeWritable is independent of DrawType. For per-frame deformation you want Dynamic so the section renders through the per-frame path (which also rebuilds the motion-vector uniform each frame). The wave example builds its grid, then creates the group compute-writable and dynamic:

// ARealtimeMeshExample_Simple_ComputeWave::OnConstruction
FRealtimeMeshStreamSet StreamSet;
TRealtimeMeshBuilderLocal<uint32, FPackedNormal, FVector2DHalf, 1> Builder(StreamSet);
Builder.EnableTangents();
Builder.EnableTexCoords();
// ... add N*N vertices (order must match the shader's index: i = GY*N + GX) and the grid triangles ...
// Compute-writable + Dynamic: Position/Tangents get UAVs the provider writes each frame.
const FRealtimeMeshBufferSetConfig Config(ERealtimeMeshSectionDrawType::Dynamic, /*bComputeWritable*/ true);
RealtimeMesh->CreateBufferSet(GetComputeWaveGroupKey(), StreamSet, Config);

The CPU-side streams you upload are the initial state and define the buffer capacities; the compute pass overwrites them on the GPU. The vertex ordering you build with must match how your shader indexes vertices.

Static geometry can be compute-writable too. The indirect example uses FRealtimeMeshBufferSetConfig(ERealtimeMeshSectionDrawType::Static, /*bComputeWritable*/ true) — it allocates the box with UAV support even though its indirect draw doesn’t strictly need it, exercising the same allocation path.


A provider records the RDG passes that write the buffers. Implement one method:

Source/RealtimeMeshComponent/Public/Compute/RealtimeMeshComputeProvider.h
class IRealtimeMeshComputeProvider
{
public:
virtual void BuildComputePasses(FRDGBuilder& GraphBuilder,
const FRealtimeMeshComputeContext& Context) = 0;
};

It’s called on the render thread, inside the frame’s RDG graph. The Context is your window onto the section’s imported buffers:

class FRealtimeMeshComputeContext
{
public:
FRDGBuilder& GetGraphBuilder() const;
const FRealtimeMeshComputeFrameInfo& GetFrame() const; // time, delta, frame counter
int32 GetVertexCapacity() const; // buffers are sized to these; you may write fewer
int32 GetIndexCapacity() const;
// The imported RDG buffer for a compute-writable stream, or nullptr if absent / not writable.
FRDGBufferRef GetStreamBuffer(const FRealtimeMeshStreamKey& Key) const;
FRDGBufferUAVRef GetStreamUAV(const FRealtimeMeshStreamKey& Key, EPixelFormat Format) const;
FRDGBufferSRVRef GetStreamSRV(const FRealtimeMeshStreamKey& Key, EPixelFormat Format) const;
// Only valid when the registration set bUseIndirect.
FRDGBufferRef GetIndirectArgsBuffer() const;
FRDGBufferUAVRef GetIndirectArgsUAV() const;
};

You ask for a stream by key (FRealtimeMeshStreams::Position, ::Tangents, ::Triangles, …) in whatever pixel format matches how you write it, then bind that UAV into your shader parameters. The driver has already imported these buffers into the graph and will transition them back to the states the base pass expects after your passes run — you just record the compute.

The shipped FRealtimeMeshWaveProvider (Source/RealtimeMeshCompute/Public/Providers/RealtimeMeshWaveProvider.h) is a complete reference. Its tunables and method:

class FRealtimeMeshWaveProvider : public IRealtimeMeshComputeProvider
{
public:
int32 GridSize = 64; // NxN vertices — must match the created mesh
float Extent = 100.0f; // half-size of the plane in local units
float Amplitude = 20.0f;
float Frequency = 0.05f;
float Speed = 2.0f;
virtual void BuildComputePasses(FRDGBuilder& GraphBuilder,
const FRealtimeMeshComputeContext& Context) override;
};

Inside BuildComputePasses it grabs the Position UAV, optionally the Tangents UAV, validates capacity, and dispatches a global compute shader:

void FRealtimeMeshWaveProvider::BuildComputePasses(FRDGBuilder& GraphBuilder, const FRealtimeMeshComputeContext& Context)
{
FRDGBufferUAVRef PositionsUAV = Context.GetStreamUAV(FRealtimeMeshStreams::Position, PF_R32_FLOAT);
if (!PositionsUAV) { return; }
FRDGBufferUAVRef TangentsUAV = Context.GetStreamUAV(FRealtimeMeshStreams::Tangents, PF_R8G8B8A8_SNORM);
const uint32 Total = uint32(GridSize) * uint32(GridSize);
if (Context.GetVertexCapacity() < int32(Total)) { return; } // never write past the buffer
// ... fill FParameters (UAVs + GridSize/Extent/Amplitude/Frequency + Frame time), then:
// FComputeShaderUtils::AddPass(GraphBuilder, ..., FIntVector(groups, 1, 1));
}

Two things worth copying: it bails cleanly if the requested stream isn’t present or the section is smaller than the grid (a mismatch would otherwise write out of bounds or leave stale data), and it reads Context.GetFrame().TimeSeconds to animate.


Build an FRealtimeMeshComputeRegistration on the game thread and register it. The registration is where you bind the provider to a specific section group and choose when it runs:

Source/RealtimeMeshComponent/Public/Compute/RealtimeMeshComputeProvider.h
struct FRealtimeMeshComputeRegistration
{
FRealtimeMeshWeakPtr Mesh; // URealtimeMesh::GetMesh() — the data object
FRealtimeMeshBufferSetKey BufferSetKey; // the compute-writable group whose streams you write
FRealtimeMeshSectionKey SectionKey; // section to bind indirect args to (bUseIndirect only)
FRealtimeMeshComputeProviderPtr Provider;
ERealtimeMeshComputeTrigger Trigger = ERealtimeMeshComputeTrigger::OnDemand;
float IntervalSeconds = 0.0f; // for the TimeInterval trigger
bool bUseIndirect = false; // allocate + bind an indirect-args buffer
uint32 IndirectArgsSizeBytes = 5 * sizeof(uint32);
};

The trigger decides cadence:

  • ERealtimeMeshComputeTrigger::EveryFrame — runs every rendered frame (use for continuous animation).
  • ERealtimeMeshComputeTrigger::OnDemand — runs once when registered, then again each time you mark it dirty.
  • ERealtimeMeshComputeTrigger::TimeInterval — runs every IntervalSeconds.

The wave example registers for every frame, in BeginPlay:

TSharedRef<FRealtimeMeshWaveProvider, ESPMode::ThreadSafe> Provider =
MakeShared<FRealtimeMeshWaveProvider, ESPMode::ThreadSafe>();
Provider->GridSize = N;
Provider->Extent = Extent;
Provider->Amplitude = Amplitude;
Provider->Frequency = Frequency;
Provider->Speed = Speed;
FRealtimeMeshComputeRegistration Registration;
Registration.Mesh = RealtimeMesh->GetMesh(); // the FRealtimeMesh data object
Registration.BufferSetKey = GetComputeWaveGroupKey(); // same key used in CreateBufferSet
Registration.Provider = Provider;
Registration.Trigger = ERealtimeMeshComputeTrigger::EveryFrame;
Registration.bUseIndirect = false;
ComputeHandle = RegisterRealtimeMeshComputeProvider(Registration); // uint64 handle; 0 == failure

The three registration functions are free functions in the RealtimeMesh:: namespace, all safe to call from the game thread (they enqueue render commands internally):

uint64 RegisterRealtimeMeshComputeProvider(const FRealtimeMeshComputeRegistration& Registration);
void UnregisterRealtimeMeshComputeProvider(uint64 Handle);
void MarkRealtimeMeshComputeProviderDirty(uint64 Handle); // request a one-shot run of an OnDemand provider

Always store the handle and unregister in EndPlay:

void ARealtimeMeshExample_Simple_ComputeWave::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (ComputeHandle != 0)
{
UnregisterRealtimeMeshComputeProvider(ComputeHandle);
ComputeHandle = 0;
}
Super::EndPlay(EndPlayReason);
}

Use MarkRealtimeMeshComputeProviderDirty(Handle) with an OnDemand provider whenever its inputs change (for example, a rebuilt SDF volume) to trigger a single regeneration without paying for a per-frame dispatch.


A surface that moves every frame will ghost under TSR/TAA unless the renderer knows each vertex’s previous position. RMC wires this up automatically if the section has a PositionPrev stream: its presence turns on per-vertex velocity output, and the compute driver maintains it for you.

Seed it at creation time as a copy of Position:

// ARealtimeMeshExample_Simple_ComputeWave::OnConstruction (gated behind bEnableMotionVectors)
if (const FRealtimeMeshStream* PositionStream = StreamSet.Find(FRealtimeMeshStreams::Position))
{
FRealtimeMeshStream PrevStream(FRealtimeMeshStreams::PositionPrev, PositionStream->GetLayout());
PrevStream.Append(*PositionStream);
StreamSet.AddStream(MoveTemp(PrevStream));
}

The FRealtimeMeshComputeFrameInfo carries a FrameCounter that must match the view’s frame counter for the prev-position gate to engage — the driver handles that plumbing; you just have to include the PositionPrev stream and use the Dynamic draw type so the per-frame path runs. Toggling the stream off is a quick way to A/B the ghosting you get without per-vertex velocity.


An indirect draw reads its draw parameters (index count, instance count, offsets) from a GPU buffer instead of the CPU, so a compute pass can decide how much to draw. There are two ways in:

1. Through a compute provider. Set bUseIndirect = true on the registration. The driver then allocates an indirect-args buffer, binds it to SectionKey (so that section renders indirect), and exposes it to your provider via Context.GetIndirectArgsBuffer() / GetIndirectArgsUAV(). Your compute pass fills the args. Size it with IndirectArgsSizeBytes (default 5 * sizeof(uint32), matching FRHIDrawIndexedIndirectParameters).

2. Directly on the section. For static control without a per-frame provider, you can set the indirect args on the section proxy yourself. ARealtimeMeshExample_Simple_ComputeIndirect does this through a proxy update task, building an args buffer whose layout is IndexCountPerInstance, InstanceCount, StartIndexLocation, BaseVertexLocation, StartInstanceLocation and handing it to the section:

// Inside a FRealtimeMeshProxyUpdateBuilder section task (render thread):
TResourceArray<uint32> Init;
Init.Add(NumIndices); // IndexCountPerInstance
Init.Add(1); // InstanceCount
Init.Add(0); // StartIndexLocation
Init.Add(0); // BaseVertexLocation
Init.Add(0); // StartInstanceLocation
FRHIBufferCreateDesc Desc = FRHIBufferCreateDesc::Create(
TEXT("RMC_ExampleIndirectArgs"), Init.GetResourceDataSize(), sizeof(uint32),
EBufferUsageFlags::Static | EBufferUsageFlags::DrawIndirect | EBufferUsageFlags::UnorderedAccess
| EBufferUsageFlags::ShaderResource | EBufferUsageFlags::VertexBuffer)
.SetInitialState(ERHIAccess::IndirectArgs)
.SetInitActionResourceArray(&Init);
FBufferRHIRef Args = RHICmdList.CreateBuffer(Desc);
Section.SetIndirectArgs(Args, 0);
// Section.ClearIndirectArgs() reverts to a normal CPU-counted draw.

The example toggles between the indirect buffer and the equivalent CPU stream-range limit (Section.UpdateStreamRange(...)) so you can verify both produce identical output. Use EBufferUsageFlags::DrawIndirect combined with the other flags as shown — a pure DrawIndirect buffer has no defined alignment on some backends and will assert.


The compute path executes on the GPU inside RDG. Headless runs with -NullRHI don’t execute the compute or draw passes, so this workflow needs a real rendering context (PIE or a running game) to see results.