Description

Sorry for the AI ticket, but it did a better job than I could:
Rendering a level sequence through Movie Render Queue crashes the editor with an EXCEPTION_ACCESS_VIOLATION when the scene contains a Niagara system using a GPU simulation target and the MRQ output format is EXR or PNG. The same render completes successfully with JPG output.

The crash is not in the image writer. It occurs in FNiagaraGpuComputeDispatch::ProcessPendingTicksFlush() — a safety-valve path that runs queued GPU simulation ticks against a fabricated dummy view outside the scene renderer. A Data Interface supplies a resource pointer that is not valid in that context; the D3D12 backend then makes a virtual call on it and jumps to address 0x1.

The output format matters only because of write latency. EXR (float, ~16 MB/frame) and 16-bit PNG stall the pipeline long enough for world ticks to accumulate without a corresponding scene render, crossing the tick-flush threshold. JPG finalizes fast enough to stay under it.


Environment

   
Engine 5.8.1-56057345 +++UE5+Release-5.8, Development Editor
OS Windows 11 24H2 — 10.0.26100.9106
GPU NVIDIA RTX A6000, driver 596.72 (2026-06-10), D3D12 / SM6
CPU / RAM AMD Ryzen Threadripper PRO 3995WX (64-core) / 256 GB
Project Flossing — level /Game/Test, sequence /Game/NewLevelSequence
Niagara system /Game/EasyRain/System/Niagara/N_EasyRain_Niagara (GPU sim target)
MRQ MovieGraph pipeline, Deferred renderer, beauty layer, 1920×1080, 1 camera, TileCount [1,1]

Note: MRQ logs Using the default MovieGraphPipeline; the pipeline specified in Project Settings (MoviePipeline) is not compatible with graphs — the project-configured pipeline class is being silently ignored. Probably unrelated, but worth a look.


Exception

From the minidump exception stream:

ExceptionCode    : 0xC0000005   EXCEPTION_ACCESS_VIOLATION
ExceptionAddress : 0x0000000000000001
Info[0]          : 8            -> EXECUTE (DEP)
Info[1]          : 0x0000000000000001

AccessType 8 is the important detail — this is an executed jump to 0x1, not a bad read. The address lies in no loaded module. That is an indirect call through a corrupt vtable pointer, i.e. a virtual call on a freed or never-initialised object.

RHI breadcrumbs (both configurations):

Breadcrumbs 'RDGExecute_RenderThread'
 - NiagaraGpuComputeDispatch
 - RenderGraphExecute
 - WorldTick

Nsight Aftermath was enabled (RHI.Aftermath true) and produced no GPU crash dump, confirming a CPU-side fault rather than a GPU fault or TDR. MemoryStats.bIsOOM = 0. Driver not denylisted.


Callstack

Default configuration crashes on RHIThread, which loses the recording context and makes the bug very hard to attribute — the only Niagara frame is a template instantiated in the Niagara module:

D3D12RHI!SetShaderParametersOnContext()                        D3D12Commands.cpp:698
D3D12RHI!FD3D12CommandContext::RHISetShaderParameters()        D3D12Commands.cpp:759
RHI!FRHICommandSetShaderParameters<FRHIComputeShader>::Execute()
Niagara!FRHICommand<...SetShaderParameters<FRHIComputeShader>...>::ExecuteAndDestruct()
RHI!FRHICommandListBase::Execute()
RHI!FRHICommandListExecutor::FTranslateState::Translate_ExecuteCommandList()
RenderCore!FRHIThread::Run()

Re-running with -norhithread -forcerhibypass moves the fault to RenderThread and exposes the recording site:

D3D12RHI!SetShaderParametersOnContext()                             D3D12Commands.cpp:698
D3D12RHI!FD3D12CommandContext::RHISetShaderParameters()             D3D12Commands.cpp:759
Niagara!FRHIComputeCommandList::SetBatchedShaderParameters()        RHICommandList.h:2631
Niagara!FNiagaraGpuComputeDispatch::DispatchStage'::<lambda_2>()    NiagaraGpuComputeDispatch.cpp:1871
Niagara!TRDGLambdaPass<...>::Execute()                              RenderGraphPass.h:704
RenderCore!FRDGBuilder::ExecutePass()                               RenderGraphBuilder.cpp:3525
RenderCore!FRDGBuilder::ExecuteSerialPass()                         RenderGraphBuilder.cpp:3549
RenderCore!FRDGBuilder::Execute()                                   RenderGraphBuilder.cpp:2157
Niagara!FNiagaraGpuComputeDispatch::ProcessPendingTicksFlush()      NiagaraGpuComputeDispatch.cpp:521
Niagara!FNiagaraGpuComputeDispatch::Tick'::<lambda_1>
RenderCore!ExecuteCommand()                                         RenderingThread.cpp:1533
RenderCore!RenderingThreadMain()                                    RenderingThread.cpp:261

Line attribution caveat: the lambda frame reports :1871 (DispatchComputeShader), but under bypass the preceding RHICmdList.SetBatchedShaderParameters(...) at :1869 is inlined into the same frame. The D3D12 frames confirm the fault is in the parameter set, not the dispatch.


Root cause

FNiagaraGpuComputeDispatch::ProcessPendingTicksFlush()NiagaraGpuComputeDispatch.cpp:385

When GPU ticks queue up without being consumed by a scene render, Tick fires a flush whose behaviour is selected by fx.Niagara.Batcher.TickFlush.Mode (default 1). The CVar's own help text documents the hazard:

fx.Niagara.Batcher.TickFlush.Mode   (default 1)
  0 = Keep ticks queued, can result in a long pause when gaining focus again.
  1 = (Default) Process all queued ticks with dummy view / buffer data,
      may result in incorrect simulation due to missing depth collisions, etc.
  2 = Kill all pending ticks, may result in incorrect simulation due to
      missing frames of data, i.e. a particle reset.

Mode 1 fabricates a view with a null render target (:474):

cpp

FSceneViewFamilyContext ViewFamily(
    FSceneViewFamily::ConstructionValues(nullptr, GetSceneInterface(), FEngineShowFlags(ESFIM_Game))
    .SetTime(CachedViewInitOptions.GameTime));
...
GetRendererModule().CreateAndInitSingleView(RHICmdList, &ViewFamily, &ViewInitOptions);
TConstStridedView<FSceneView> DummyViews = MakeStridedView<const FSceneView>(0, ViewFamily.Views[0], 1);

bIsOutsideSceneRenderer = true;   // "Allow downstream logic to detect we are
                                  //  running pending ticks outside the scene renderer"

It then drives the full PreInitViews / PostInitViews / PostRenderOpaque chain against that dummy view. DispatchStage populates the batched parameters via SetShaderParameters(...) (:1866), which walks the shader parameter metadata including every Data Interface binding, and submits them at :1869. One of those bindings is invalid in the dummy-view context, and SetShaderParametersOnContext dereferences it.

The core defect: bIsOutsideSceneRenderer is only consulted in two places in the entire file (:1981 and :2143). It is never plumbed through to Data Interfaces, so a DI that binds a scene texture, depth buffer, or distance field has no way to detect the dummy-view context and bail out. The comment on :496 states the flag exists precisely so "downstream logic" can detect this — but downstream DI parameter population does not check it. The result is a corrupt pointer rather than the merely-incorrect simulation the CVar comment leads you to expect.

Trigger thresholds

fx.Niagara.Batcher.TickFlush.MaxQueuedFrames   default 3
fx.Niagara.Batcher.TickFlush.MaxPendingTicks   default 10

Either crossing trips the flush (:405, :418). The MaxQueuedFrames help text says it is "generally only a concern when the application does not have focus" — the path was designed for a minimised editor, not for MRQ, which legitimately ticks the world without rendering every tick (warm-up frames, temporal sub-samples, and finalization).

Why the output format correlates

Not alpha, and not bit depth — finalize/write duration. EXR and 16-bit PNG hold the pipeline long enough for ≥3 world ticks to elapse with no scene render, crossing MaxQueuedFrames. JPG does not. This also explains why the first crash landed exactly at MRQ's Moving to Finalize to finish writing items to disk — MRQ stops rendering the scene there while the world keeps ticking, so the threshold is crossed within a few frames.

Frequency

10 for 10 on 2026-08-18 between 10:59 and 11:40, all with an identical signature. Crash GUIDs in Flossing/Saved/Crashes:

0BF6DEB74DCE9C7AB3F1528D7896E696    RHIThread
7D28BEFC49E34ABB06214E92DFDD13B8    RHIThread
CA32B18048CB92979C8949B4F1D8B80F    RHIThread
493E616E4BD781DFE0F03095951E1E4F    RHIThread
6623D4FA464F18F9CED47EA9575F85D5    RHIThread
12BDB7D44C1E68A13674C48FA4370000    RHIThread
4E2B020A448915A91F9EF28233C0A07B    RHIThread
7156B1F74D6994B31C5AA99A25EB1540    RHIThread
DAB4A6DF4DB07931347BBBB81FD5C428    RHIThread
B8737D1445050FC93078BBA71C33A599    RenderThread  (-norhithread -forcerhibypass)

Diagnosis aid

The default RHIThread stack is effectively undebuggable for this class of bug — the resource is bound on the render thread and dereferenced later on the RHI thread, so the recording context is gone. Relaunching with the following moves the fault to the recording site and was what made this diagnosable:

UnrealEditor.exe <Project>.uproject -norhithread -forcerhibypass

(-forcerhibypassRHICommandList.cpp:1886. r.RHICmdBypass alone is a silent no-op while the RHI thread runs, because LatchBypass() at :1897 requires !IsRunningRHIInSeparateThread().)


Workarounds

Recommended — prevents the flush from firing during a render, keeps GPU particles and correct simulation:

fx.Niagara.Batcher.TickFlush.MaxQueuedFrames 100000
fx.Niagara.Batcher.TickFlush.MaxPendingTicks 100000

Confirms the diagnosis — avoids the dummy-view path entirely:

fx.Niagara.Batcher.TickFlush.Mode 0     # keep ticks queued
fx.Niagara.Batcher.TickFlush.Mode 2     # discard pending ticks (visible particle reset)

Bluntfx.NiagaraAllowGPUParticles 0. Avoids the crash but removes the GPU-simulated effect from the render entirely, so it is not viable for delivery.


Suggested fix

  1. Plumb IsOutsideSceneRenderer() into Data Interface shader-parameter population so DIs depending on scene textures, depth, or distance fields can substitute a safe fallback binding. The flag already exists and is documented as being for exactly this purpose, but is only read in two places.
  2. Validate resource pointers before binding in ProcessPendingTicksFlush's dispatch path, or skip stages whose DI bindings cannot be satisfied outside the scene renderer. Degrading to an incorrect simulation — which the CVar comment already sanctions — is strictly better than an access violation.
  3. Consider having MRQ suppress or raise the Niagara tick-flush thresholds for the duration of a render job. MRQ intentionally ticks the world without rendering every tick, so it will always be liable to trip a heuristic tuned for an unfocused editor.
  4. checkf(Handle.IsValid(), ...) at D3D12Commands.cpp:673 catches this class of problem in the bindless path. The non-bindless path immediately below (:679 onward) has no equivalent guard and dereferences straight away.

[Link Removed]
[Link Removed]
[Link Removed]
[Link Removed]
[Link Removed]

Steps to Reproduce
  • Project containing a Niagara system with Sim Target = GPU in the rendered level (reproduced with EasyRain / N_EasyRain_Niagara).
  • Create a level sequence with a CineCameraActor and an MRQ MovieGraph job.
  • Deferred renderer, beauty layer, 1920×1080, single camera, no high-res tiling.
  • Set output format to EXR or PNG. Render.
  • Editor crashes with the access violation above, at or shortly before finalization.
  • Change output format to JPG only. Render. Completes successfully.

To make the mechanism explicit rather than timing-dependent, lower the threshold so the flush fires reliably:

fx.Niagara.Batcher.TickFlush.MaxQueuedFrames 1

Have Comments or More Details?

There's no existing public thread on this issue, so head over to Questions & Answers just mention UE-392456 in the post.

0
Login to Vote

Unresolved
CreatedAug 18, 2026
UpdatedAug 20, 2026
View Jira Issue