<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saish</title>
    <description>The latest articles on DEV Community by Saish (@saish_in3d).</description>
    <link>https://dev.to/saish_in3d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177432%2F407b5a94-d243-49d4-a2d0-f79830306a0a.jpeg</url>
      <title>DEV Community: Saish</title>
      <link>https://dev.to/saish_in3d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saish_in3d"/>
    <language>en</language>
    <item>
      <title>Unreal Engine GPU Stack: Chapter 2 Scene View Extensions</title>
      <dc:creator>Saish</dc:creator>
      <pubDate>Sun, 25 Oct 2026 20:43:54 +0000</pubDate>
      <link>https://dev.to/saish_in3d/unreal-engine-gpu-stack-chapter-2-scene-view-extensions-d44</link>
      <guid>https://dev.to/saish_in3d/unreal-engine-gpu-stack-chapter-2-scene-view-extensions-d44</guid>
      <description>&lt;p&gt;&lt;em&gt;How to inject a custom rendering pass into Unreal's frame without modifying the engine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In Chapter 1 we did all the required world building... three threads, the RHI, and the Render Dependency Graph. The next practical question is how your own GPU work gets &lt;em&gt;into&lt;/em&gt; Unreal's frame.&lt;/p&gt;

&lt;p&gt;There are a few common answers from Youtube tutorials, and none of them are perfect. A post-process material can't run a compute shader or write to an render target. A Scene Capture Component renders the whole scene again from another view just to hand you pixels you already had. &lt;/p&gt;

&lt;p&gt;Imagine overlying your active digital twin data over real world CCTV footage. Or building a cam overlay over Unreal Automotive HMD or just compositing real world background to generate synthetic data. All can be done efficiently using GPUCompositionGraphicsPipeline or SynthPass. More about this in this chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What's happening in GPUCompositionGraphicsPipeline
&lt;/h2&gt;

&lt;p&gt;This chapter uses one plugin as its running example: &lt;code&gt;GPUCompositionGraphicsPipeline&lt;/code&gt;. It takes a live video feed (anything that plays into a &lt;code&gt;UMediaTexture&lt;/code&gt;) and composites the rendered frame over it. All of it runs on the GPU, in the middle of Unreal's own post-processing.&lt;/p&gt;

&lt;p&gt;It has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UCompGraphicsSubsystem&lt;/code&gt;&lt;/strong&gt;, an engine subsystem that runs on the Game thread. This is the Blueprints facing side. It creates and owns the extension, holds the media texture and the output render target, and tags the actors that should appear in the composite with custom stencil value &lt;code&gt;20&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FCoreSVE&lt;/code&gt;&lt;/strong&gt;, the Scene View Extension. It subscribes to the post-processing stage right after FXAA and runs the compositing callback on the Render thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two passes inside that callback.&lt;/strong&gt; A pixel shader (&lt;code&gt;FCustomDepthStencilMaskCreatorPS&lt;/code&gt;) turns the custom stencil buffer into a mask. Then a compute shader (&lt;code&gt;FCompositorCS&lt;/code&gt;) blends scene color, the mask and the media texture. The result is drawn to the viewport and copied into a render target, so it can be used outside the viewport too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in one frame, the Game thread only sets values. On the Render thread, the renderer asks &lt;code&gt;FCoreSVE&lt;/code&gt; where it wants to run. After FXAA it adds the mask pass, then the compositing pass, then the copy and draw. Every snippet below is from this plugin unless it's marked as engine source.&lt;/p&gt;

&lt;p&gt;Engine code below is from UE 5.6&lt;/p&gt;

&lt;p&gt;The interface lives in the &lt;code&gt;Engine&lt;/code&gt; module (&lt;code&gt;Engine/Public/SceneViewExtension.h&lt;/code&gt;), but anything useful you do inside it needs the rendering modules. The plugin's &lt;code&gt;Build.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GPUCompositionGraphicsPipeline.Build.cs&lt;/span&gt;
&lt;span class="n"&gt;PublicDependencyModuleNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"Core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"CoreUObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ISceneViewExtension, FSceneViewExtensionBase&lt;/span&gt;
    &lt;span class="s"&gt;"RenderCore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// FRDGBuilder, global shaders&lt;/span&gt;
    &lt;span class="s"&gt;"Renderer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// FScreenPassTexture, FPostProcessMaterialInputs&lt;/span&gt;
    &lt;span class="s"&gt;"RHI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// textures, formats, feature levels&lt;/span&gt;
    &lt;span class="s"&gt;"Projects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// IPluginManager, for the shader directory mapping&lt;/span&gt;
    &lt;span class="s"&gt;"MediaAssets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// UMediaTexture, the video input&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/GPUCompositionGraphicsPipeline.Build.cs#L12-L21" rel="noopener noreferrer"&gt;&lt;code&gt;GPUCompositionGraphicsPipeline.Build.cs&lt;/code&gt; L12–L21&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The headers you'll typically include:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj79bo3kbzn6g8bv5jzl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj79bo3kbzn6g8bv5jzl4.png" alt="Scene View Extension headers" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One header to be careful with: &lt;code&gt;FPostProcessingInputs&lt;/code&gt;, the argument to &lt;code&gt;PrePostProcessPass_RenderThread&lt;/code&gt;, is defined in &lt;code&gt;Renderer/Internal/PostProcess/PostProcessInputs.h&lt;/code&gt;. You can override the hook with only a forward declaration. But if you want to read its members, you have to add &lt;code&gt;Renderer/Internal&lt;/code&gt; to your include paths, and nothing in &lt;code&gt;Internal&lt;/code&gt; or &lt;code&gt;Private&lt;/code&gt; is a stable API. Stay in &lt;code&gt;Public&lt;/code&gt; wherever you can.&lt;/p&gt;

&lt;p&gt;The plugin does take that trade-off. It includes &lt;code&gt;PostProcess/PostProcessInputs.h&lt;/code&gt; and a few other renderer headers, so it adds both paths. It builds on 5.6, but an engine upgrade can break it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;EngineDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFullPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelativeEnginePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;PrivateIncludePaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EngineDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Source/Runtime/Renderer/Private"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EngineDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Source/Runtime/Renderer/Internal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/GPUCompositionGraphicsPipeline.Build.cs#L36-L45" rel="noopener noreferrer"&gt;&lt;code&gt;GPUCompositionGraphicsPipeline.Build.cs&lt;/code&gt; L36–L45&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shaders have to be found before they compile.&lt;/strong&gt; If the extension uses global shaders from your plugin, the &lt;code&gt;Shaders&lt;/code&gt; directory mapping from Chapter 1 must exist before the engine compiles global shaders, which happens early in startup. Thus, such modules use &lt;code&gt;"LoadingPhase": "PostConfigInit"&lt;/code&gt; in the &lt;code&gt;.uplugin&lt;/code&gt;. With &lt;code&gt;Default&lt;/code&gt;, the engine can't resolve your virtual shader path. The plugin's &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/GPUCompositionGraphicsPipeline.uplugin#L21" rel="noopener noreferrer"&gt;&lt;code&gt;.uplugin&lt;/code&gt;&lt;/a&gt; sets &lt;code&gt;PostConfigInit&lt;/code&gt;, and &lt;code&gt;StartupModule&lt;/code&gt; does only the mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GPUCompositionGraphicsPipeline.cpp&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FGPUCompositionGraphicsPipelineModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;StartupModule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;FString&lt;/span&gt; &lt;span class="n"&gt;PluginShaderDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FPaths&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IPluginManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;FindPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GPUCompositionGraphicsPipeline"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetBaseDir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shaders"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;AddShaderSourceDirectoryMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/Plugins/GPUCompositionGraphicsPipeline"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;PluginShaderDir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/GPUCompositionGraphicsPipeline.cpp#L7-L12" rel="noopener noreferrer"&gt;&lt;code&gt;GPUCompositionGraphicsPipeline.cpp&lt;/code&gt; L7–L12&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There was a second problem I didn't expect here: mapping the shader directory and creating the extension can't happen at the same point in startup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Engine source: Engine/Private/SceneViewExtension.cpp&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FSceneViewExtensions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RegisterExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneViewExtensionRef&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;RegisterMe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ensure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GEngine&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;KnownExtensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GEngine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ViewExtensions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;KnownExtensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// ... compact dead entries ...&lt;/span&gt;
        &lt;span class="n"&gt;KnownExtensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddUnique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterMe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A module loading at &lt;code&gt;PostConfigInit&lt;/code&gt; starts long before &lt;code&gt;GEngine&lt;/code&gt; exists. So map the shader directory in &lt;code&gt;StartupModule&lt;/code&gt;, but create the extension later... in a subsystem's &lt;code&gt;Initialize&lt;/code&gt;, from &lt;code&gt;FCoreDelegates::OnPostEngineInit&lt;/code&gt;, or when gameplay asks for it. The plugin uses the last option: nothing is created until a Blueprint calls &lt;code&gt;InitilizeCompositor&lt;/code&gt; on the subsystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Creating and owning an extension
&lt;/h2&gt;

&lt;p&gt;The plugin's extension, trimmed to what it uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CoreGraphicsPipeline.h&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FCoreSVE&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;FSceneViewExtensionBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FAutoRegister&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;AutoRegister&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;SubscribeToPostProcessingPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPostProcessingPass&lt;/span&gt; &lt;span class="n"&gt;PassId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneView&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;FAfterPassCallbackDelegateArray&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;InOutPassCallbacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bIsPassEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt; &lt;span class="n"&gt;GraphicsCompositing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FRDGBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneView&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FPostProcessMaterialInputs&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// CoreGraphicsPipeline.cpp&lt;/span&gt;
&lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FAutoRegister&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;AutoRegister&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FSceneViewExtensionBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AutoRegister&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Public/CoreGraphicsPipeline.h#L10-L29" rel="noopener noreferrer"&gt;&lt;code&gt;CoreGraphicsPipeline.h&lt;/code&gt; L10–L29&lt;/a&gt;, &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CoreGraphicsPipeline.cpp#L27-L31" rel="noopener noreferrer"&gt;&lt;code&gt;CoreGraphicsPipeline.cpp&lt;/code&gt; L27–L31&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FAfterPassCallbackDelegateArray&lt;/code&gt; is the older name. In 5.6 it's an alias for &lt;code&gt;FPostProcessingPassDelegateArray&lt;/code&gt;, so both compile.&lt;/p&gt;

&lt;p&gt;And creating it, from the subsystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CompGraphicsSubsystem.h&lt;/span&gt;
&lt;span class="n"&gt;TSharedPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FCoreSVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ESPMode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ThreadSafe&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CoreSVE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// CompGraphicsSubsystem.cpp&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UCompGraphicsSubsystem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InitilizeCompositor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Custom depth with stencil, which the mask pass reads&lt;/span&gt;
    &lt;span class="n"&gt;UKismetSystemLibrary&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ExecuteConsoleCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetWorld&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;FString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"r.CustomDepth 3"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;CoreSVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FSceneViewExtensions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NewExtension&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Public/CompGraphicsSubsystem.h#L87" rel="noopener noreferrer"&gt;&lt;code&gt;CompGraphicsSubsystem.h&lt;/code&gt; L87&lt;/a&gt;, &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CompGraphicsSubsystem.cpp#L26-L34" rel="noopener noreferrer"&gt;&lt;code&gt;CompGraphicsSubsystem.cpp&lt;/code&gt; L26–L34&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FAutoRegister&lt;/code&gt; has a private constructor, and only &lt;code&gt;FSceneViewExtensions&lt;/code&gt; can create one. So &lt;code&gt;new FCoreSVE(...)&lt;/code&gt; doesn't compile. The only way in is &lt;code&gt;NewExtension&amp;lt;T&amp;gt;()&lt;/code&gt;, which constructs the extension, wraps it in a thread-safe shared pointer, and registers it in one step. The first constructor argument must be &lt;code&gt;FAutoRegister&lt;/code&gt;. Any extra arguments you pass to &lt;code&gt;NewExtension&lt;/code&gt; are forwarded after it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership is yours.&lt;/strong&gt; The engine's registry only holds &lt;em&gt;weak&lt;/em&gt; pointers. If you drop your shared pointer, the extension is gone from the next frame on, and that's also how you unregister it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;UCompGraphicsSubsystem&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DeinitializeCompositor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CoreSVE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CoreSVE&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IsActiveThisFrameFunctions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;CoreSVE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... release the render target and show-only actors ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CompGraphicsSubsystem.cpp#L77-L95" rel="noopener noreferrer"&gt;&lt;code&gt;CompGraphicsSubsystem.cpp&lt;/code&gt; L77–L95&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The frame already in flight is safe (the frame that is being rendered in gpu). Each view family copies the active extensions into &lt;code&gt;ViewFamily.ViewExtensions&lt;/code&gt; as &lt;em&gt;strong&lt;/em&gt; references, so the Render thread keeps the extension alive until that frame finishes. This is the same idea as the scene proxies in Chapter 1: the Game thread can let go while the Render thread finishes using it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ESPMode::ThreadSafe&lt;/code&gt; isn't optional. The pointer is created on the Game thread and used on the Render thread, so its reference count must be atomic.&lt;/p&gt;

&lt;p&gt;Who should hold the pointer? Something that lives as long as the effect should: an engine subsystem for effects that persist across levels, a world subsystem for per-level effects, or an actor or component when the effect belongs to one object. The plugin uses an engine subsystem, so the compositor survives level changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Every injection point in the frame
&lt;/h2&gt;

&lt;p&gt;The hooks follow the frame from Game-thread setup to the end of rendering. Let's see which thread calls them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa63jwgntuv1u1e64u84b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa63jwgntuv1u1e64u84b.png" alt="Where a Scene View Extension runs in one frame" width="800" height="992"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Game thread hooks: shaping the MVP
&lt;/h3&gt;

&lt;p&gt;These hooks run while the engine builds the view family for the &lt;em&gt;next&lt;/em&gt; frame. They don't take an &lt;code&gt;FRDGBuilder&lt;/code&gt;, so you can't add passes here. Their job is to change &lt;em&gt;what&lt;/em&gt; gets rendered, not &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xpf19st93zyia6hiu1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xpf19st93zyia6hiu1q.png" alt="Game thread hooks" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's take a closer look at &lt;code&gt;BeginRenderViewFamily&lt;/code&gt; . It's the last point where you're on the Game thread and know that a frame is about to be rendered. It's the correct place to copy settings, &lt;code&gt;UObject&lt;/code&gt; data and texture resource pointers into a plain struct that the Render-thread hooks will read. Reading &lt;code&gt;UObject&lt;/code&gt;s directly from Render-thread hooks is the kind of data race Chapter 1 warned about. It usually works, until the Game thread changes the object in the middle of a frame. You also need to consider the copy cost for data from one thread to another. It a tradeoff between performance and unsafe code (Just like rust providing memory safety at compile times but programmers need to write unsafe() code at some places to extract every drop of perf)&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Render thread: the scene renderer
&lt;/h3&gt;

&lt;p&gt;These hooks receive an &lt;code&gt;FRDGBuilder&amp;amp;&lt;/code&gt;. As Chapter 1 explained, nothing runs when you call RDG. You're adding passes to a graph that executes later, and the graph orders them after whatever came before your hook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpro18bdalrnsvirqispn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpro18bdalrnsvirqispn.png" alt="Render thread hooks" width="799" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see that the per-view hooks (&lt;code&gt;PreRenderView&lt;/code&gt;, &lt;code&gt;PrePostProcessPass&lt;/code&gt;, &lt;code&gt;PostRenderView&lt;/code&gt;) run once for &lt;em&gt;each&lt;/em&gt; view. Split screen, stereo rendering and scene captures with multiple views all call them more than once per frame, so don't assume one call means one frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Injection points inside post-processing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SubscribeToPostProcessingPass&lt;/code&gt; works differently from the other hooks. It doesn't render anything. At the start of post-processing, the renderer calls it once per view for each stage and asks "do you want to run something here?". You answer by adding a delegate. The plugin answers only for the FXAA stage, and only while the compositor is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CoreGraphicsPipeline.cpp&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SubscribeToPostProcessingPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPostProcessingPass&lt;/span&gt; &lt;span class="n"&gt;PassId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneView&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FAfterPassCallbackDelegateArray&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;InOutPassCallbacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bIsPassEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UCompGraphicsSubsystem&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;CompGraphicsSubsystem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GEngine&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetEngineSubsystem&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UCompGraphicsSubsystem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;CompGraphicsSubsystem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;CompGraphicsSubsystem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetShouldCompositorRun&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PassId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EPostProcessingPass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FXAA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;InOutPassCallbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FAfterPassCallbackDelegate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CreateRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FCoreSVE&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GraphicsCompositing&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CoreGraphicsPipeline.cpp#L39-L51" rel="noopener noreferrer"&gt;&lt;code&gt;CoreGraphicsPipeline.cpp&lt;/code&gt; L39–L51&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This hook runs on the Render thread, and it reads a &lt;code&gt;UObject&lt;/code&gt; (the subsystem) directly. That's the race described above. It works because a bool flag is rarely changed mid-frame. The safer version copies the flag in &lt;code&gt;BeginRenderViewFamily&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These are the stages in 5.6, in the order they run in the frame. The engine header notes that each one sits immediately before a post-process material blendable location, so if you know the material blend locations, you already know these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcyshl64hyn419z94w0vl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcyshl64hyn419z94w0vl.png" alt="Post-processing injection stages" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The plugin's stage, FXAA, comes after tonemapping. So the composite blends display-ready colors with the video, which is the space the footage is already in. Here's the FXAA case from &lt;code&gt;PostProcessing.cpp&lt;/code&gt;, because it surprises people:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Engine source: Renderer/Private/PostProcess/PostProcessing.cpp&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PassSequence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FXAA&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SceneColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AddFXAAPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PassInputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;SceneColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AddAfterPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FXAA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SceneColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// your callbacks, unconditionally&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the reason why the plugin's callback still runs when the project uses TSR and FXAA is off. The &lt;code&gt;bIsPassEnabled&lt;/code&gt; argument tells you whether the pass itself is running this frame. In the engine's recommendation, subscribe only when you'll actually produce a GPU resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 The contract for a post-processing callback
&lt;/h3&gt;

&lt;p&gt;Every stage uses the same delegate signature. The plugin's callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt; &lt;span class="nf"&gt;GraphicsCompositing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FRDGBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneView&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FPostProcessMaterialInputs&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Public/CoreGraphicsPipeline.h#L24" rel="noopener noreferrer"&gt;&lt;code&gt;CoreGraphicsPipeline.h&lt;/code&gt; L24&lt;/a&gt;, &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CoreGraphicsPipeline.cpp#L53-L113" rel="noopener noreferrer"&gt;&lt;code&gt;CoreGraphicsPipeline.cpp&lt;/code&gt; L53–L113&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three rules to keep in mind while setting a callback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You receive scene color and must return scene color.&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;Multiple subscribers to the same stage run in priority order, and each one gets the previous one's output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OverrideOutput&lt;/code&gt; is only valid if you're last.&lt;/strong&gt; For the later stages (from &lt;code&gt;ReplacingTonemapper&lt;/code&gt; onwards), your callback may be the final pass that writes to the back buffer. In that case &lt;code&gt;Inputs.OverrideOutput&lt;/code&gt; is valid and you &lt;strong&gt;must&lt;/strong&gt; write into it. Otherwise it's invalid and you create your own output (Creates a black screen on viewport).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;GraphicsCompositing&lt;/code&gt; adds the mask pass and then hands off to &lt;code&gt;FCompositorCS::ImplementCompShader&lt;/code&gt;, which handles the output. Trimmed to the parts that follow the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CompositorCS.cpp&lt;/span&gt;
&lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt; &lt;span class="n"&gt;FCompositorCS&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ImplementCompShader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FRDGBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FSceneView&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;SceneView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FPostProcessMaterialInputs&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ...mask, media texture, blend values... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rule 1: take scene color in&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;SceneColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CopyFromSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPostProcessMaterialInput&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SceneColor&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 3: write into OverrideOutput if we're last, otherwise make our own target&lt;/span&gt;
    &lt;span class="n"&gt;FScreenPassRenderTarget&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OverrideOutput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FScreenPassRenderTarget&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CreateFromInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SceneColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ERenderTargetLoadAction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ENoAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CompositorViewportOutput"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Compositing compute pass writes OutputTexture, then it's copied into the output render target.&lt;/span&gt;
    &lt;span class="c1"&gt;// (Chapter 3 covers writing the shader and adding the pass.)&lt;/span&gt;

    &lt;span class="n"&gt;AddDrawTexturePass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SceneView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OutputTexture&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Rule 1: hand scene color back&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bShallCompOutputOnViewport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FScreenPassTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SceneColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Source in SynthPass: &lt;a href="https://github.com/Saish-in3d/SynthPass/blob/a45151350991c5cbf54857dbea87fc5f7d6c4e4b/Source/GPUCompositionGraphicsPipeline/Private/CompositorCS.cpp#L14-L140" rel="noopener noreferrer"&gt;&lt;code&gt;CompositorCS.cpp&lt;/code&gt; L14–L140&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What's next
&lt;/h2&gt;

&lt;p&gt;This chapter covered where your work can go in the frame. It skipped &lt;em&gt;what&lt;/em&gt; that work is: the shader, its parameter struct, and how a pass is added to the graph. Chapter 3 covers that with compute shaders: declaring a global shader in a plugin, binding its parameters, dispatching it through RDG from one of the hooks above, and what happens between &lt;code&gt;FComputeShaderUtils::AddPass&lt;/code&gt; and threads running on an SM.&lt;/p&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>gpu</category>
      <category>graphics</category>
    </item>
    <item>
      <title>Unreal Engine GPU Stack: Chapter 1 World Building and Context Setup</title>
      <dc:creator>Saish</dc:creator>
      <pubDate>Sun, 18 Oct 2026 20:56:23 +0000</pubDate>
      <link>https://dev.to/saish_in3d/unreal-engine-gpu-stack-chapter-1-world-building-and-context-setup-1d9</link>
      <guid>https://dev.to/saish_in3d/unreal-engine-gpu-stack-chapter-1-world-building-and-context-setup-1d9</guid>
      <description>&lt;p&gt;In the prologue, moving a per-pixel workload from Tick into a compute shader took a scene from 7 FPS to 65 FPS. That result worked. What it didn't give me was an explanation. It was a Blackbox for me. I knew the shader ran, but I couldn't say which thread dispatched it, when it actually executed on the GPU.&lt;/p&gt;

&lt;p&gt;Before writing more GPU code, I needed some kind of a world context building. What happens between "I changed a value on an actor" and "a pixel changed on screen"? What's happening inside at a lower level?&lt;/p&gt;

&lt;p&gt;This chapter builds that world along with fundamentals. There isn't much shader code here. The goal is to have a mental model solid enough that when something goes wrong in later chapters, you can guess where it went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bit of high school level of boring history for better context
&lt;/h2&gt;

&lt;p&gt;Initially, the CPU did all the math (SISD) needed to draw graphics. It worked, but became too slow as graphics got more complex.&lt;/p&gt;

&lt;p&gt;People realized that graphics requires doing the "same calculations many times with different data on different threads" (SIMT) ( moving vertices, multiplying matrices, calculating pixels, and so on). So they built specialized hardware that could do many of these calculations "at the same time".&lt;/p&gt;

&lt;p&gt;That specialized hardware eventually became what we know today as "GPUs".&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ZrJeYFxpUyQ" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 The narrative SISD vs SIMD vs SIMT
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SISD (Single Instruction, Single Data):&lt;/strong&gt; One instruction works on one piece of data at a time. This is the classic scalar CPU model... add two numbers, then the next two, then the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIMD (Single Instruction, Multiple Data):&lt;/strong&gt; One instruction works on a small fixed-width vector of data at once, such as 4, 8 or 16 floats with SIMD units on a CPU. You (or the compiler) pack the data into vector registers explicitly. (registers are the most smallest, fastest and closest data storage to core or hardware that does compute)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIMT (Single Instruction, Multiple Threads):&lt;/strong&gt; The GPU's model. Threads aren't scheduled individually. They're bundled into groups of 32 (a &lt;em&gt;warp&lt;/em&gt; on NVIDIA) or 32/64 (a &lt;em&gt;wavefront&lt;/em&gt; on AMD), and every thread in the group executes the same instruction at the same time, each on its own data. You write the shader as if it were one thread handling one pixel, and the hardware runs it 32 or 64 at a time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vnqw4jjuj3nu7l1iomn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vnqw4jjuj3nu7l1iomn.png" alt="SISD vs SIMD vs SIMT" width="799" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Character building: Understanding the hardware and its workings technically, CPU, GPU and PCIe bridge
&lt;/h2&gt;

&lt;p&gt;Before looking at Unreal's graphics system, it helps to know what they are ultimately feeding. A CPU and a GPU are both processors that fetch instructions and do math on data, but they were built for different purposes. The CPU is built to finish one task as fast as possible. The GPU is built to finish as many tasks as possible in a given time. &lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 The CPU: optimized for latency
&lt;/h3&gt;

&lt;p&gt;A modern desktop CPU has somewhere between 8 and 24 physical cores, running at roughly 4 to 6 GHz. Each core is a very complex machine. It predicts which way branches will go before they're evaluated, reorders and schedules instructions so independent work runs while it waits on slow work, and runs several instructions per clock cycle in a single core through multiple execution sub units.&lt;/p&gt;

&lt;p&gt;Most of that complex stuff above exists to hide one problem: memory is slow. Reading from main memory takes around 70 to 100 nanoseconds, which is several hundred clock cycles staying idle. So CPUs has dedicated hardware for caches.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k5pa3y63nhmm904lsof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3k5pa3y63nhmm904lsof.png" alt="CPU memory hierarchy" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Data moves between these levels in 64-byte cache lines, not individual values (like tetris but all blocks are horizontal lines). Read one float and its fifteen neighbors come along for free. Code that reads memory in order gets that benefit, while code that jumps around (pointer chasing through randomly spawned actors and components, for example) keeps paying the full trip to DRAM.&lt;/p&gt;

&lt;p&gt;The CPU is great at branchy, unpredictable, serial logic i.e. gameplay, AI, physics queries, the scene management the Render thread does. But it doesn't have width. Eight cores with 8-wide SIMD compute unit gives you roughly 64 floats being processed per cycle at best. A 4k image has just over eight million pixels. That's the workload from the prologue, and that's why it hit a wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 The GPU: optimised for throughput
&lt;/h3&gt;

&lt;p&gt;A GPU does things differently. It runs at lower clocks, roughly 1.5 to 2.8 GHz, and each individual core is simple... no branch prediction like CPU, small caches per core. Instead what it has  is &lt;strong&gt;a huge number of small parallel computing units&lt;/strong&gt;. Modern highend GPUs have thousands of them, grouped into larger units such as &lt;strong&gt;Streaming Multiprocessors (NVIDIA)&lt;/strong&gt; or &lt;strong&gt;Compute Units (AMD)&lt;/strong&gt;. This lets them run &lt;strong&gt;tens of thousands of threads at the same time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A modern GPU is more than just thousands of compute cores. It contains different types of hardware, each designed for a specific kind of work. &lt;strong&gt;CUDA/Shader cores&lt;/strong&gt; handle general calculations and graphics, &lt;strong&gt;Tensor Cores&lt;/strong&gt; accelerate AI and matrix operations, &lt;strong&gt;RT Cores&lt;/strong&gt; speed up ray tracing, while dedicated hardware handles tasks like video encoding, decoding, and data transfers. Together, these specialised parts make GPUs efficient at handling many different workloads in parallel.&lt;/p&gt;

&lt;p&gt;All of this just to achieve SIMT! &lt;/p&gt;

&lt;p&gt;Few questions that clears the fog about GPU, streaming multiprocessors and warps/wavefont.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;is a streaming multiprocessor actual hardware on GPU? Yes. A Streaming Multiprocessor (SM) is an actual physical hardware block inside an NVIDIA GPU. it is structured like this.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj3wq9tgqvi8p5drv0tj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frj3wq9tgqvi8p5drv0tj.png" alt="Streaming Multiprocessor layout" width="799" height="630"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;is warp  just a driver level abstraction?&lt;br&gt;
No. A warp is basically a GPU hardware/architecture concept, exposed to programmers through programming. There is a specific warp scheduler hardware (orange in above pic) that handles thread allocation. If a SM has 1024 threads then it can 32 warps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;can a warp contain threads from two different SM?&lt;br&gt;
No. A warp stays entirely on one SM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;does rendering and compute shaders run on same SM's?&lt;br&gt;
Yes. Graphics shaders and compute shaders generally use the same SMs/execution resources on a GPU. Although there is dedicated hardware to perform some fixed function operations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are more nuances to this, we will address them in detail when we cover compute shaders in future chapters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 The bridge: PCIe
&lt;/h3&gt;

&lt;p&gt;On a desktop PC, the CPU and a discrete GPU are separate devices with separate memory, connected by PCI Express. This is the narrowest pipe in the whole system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9gjnpigdpdirikqfomo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9gjnpigdpdirikqfomo.png" alt="PCIe bandwidth" width="799" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 4K render target in a 16-bit float format is about 66 MB. Copying it across PCIe 4.0 takes around 2.6 ms, roughly 16% of a 60 FPS frame budget, before the GPU has done any work on it. The same texture read from VRAM by a shader is a lot cheaper. The lesson is simple... data should cross the bus as rarely as possible. Upload once, keep it on the GPU, and only bring results back when something on the CPU truly needs them.&lt;/p&gt;

&lt;p&gt;Bandwidth is not the only cost. Every transfer and every dispatch also has fixed overhead... driver work, command submission, synchronization. For small workloads that overhead dominates, which is why a tiny compute shader can end up slower than doing the same work on the CPU.&lt;/p&gt;

&lt;p&gt;It's also why the two processors don't talk to each other directly. The CPU writes commands into a buffer, and the GPU consumes that buffer later, usually a frame or more behind. Asking the GPU for an answer "right now" means the CPU has to stop and wait for the GPU to catch up, and the GPU sits idle until the CPU gives it more work. That's the same stall that shows up later as&amp;nbsp;&lt;code&gt;FlushRenderingCommands()&lt;/code&gt;&amp;nbsp;and as naive readback, and the whole thread architecture in the next section exists to avoid it.&lt;/p&gt;

&lt;p&gt;Not every platform works like this. Consoles, Apple Silicon and most mobile chips use&amp;nbsp;&lt;strong&gt;unified memory&lt;/strong&gt;, where CPU and GPU share the same physical RAM. There's no PCIe copy, but they compete for the same bandwidth (most likely unified memories have lower speeds than dedicated vram and HBMs). Mobile GPUs also usually use&amp;nbsp;&lt;strong&gt;tile-based rendering&lt;/strong&gt;, which processes the screen in small tiles held in on-chip memory. That changes which techniques are cheap and which are expensive. This series targets desktop hardware, but it's worth remembering that "the GPU" is not one architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Three threads and a few transfers
&lt;/h2&gt;

&lt;p&gt;The first thing that changed how I read Unreal's rendering code was realising that a frame isn't produced by one thread. There are literally three threads involved.&lt;/p&gt;

&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Game thread&lt;/strong&gt;&amp;nbsp;runs gameplay... actors tick, components update, Blueprints execute (something similar to updating UBO info in Vulkan terms). The&amp;nbsp;&lt;strong&gt;Render thread&lt;/strong&gt;&amp;nbsp;takes the resulting scene state and decides what to draw and in what order (Holds and manages vertex proxies or host(cpu) native/ staging VBO in vulkan terms). The&amp;nbsp;&lt;strong&gt;RHI thread&lt;/strong&gt;&amp;nbsp;translates those decisions into actual D3D12, Vulkan or Metal commands and submits them to the driver. The GPU then executes them on its own schedule.&lt;/p&gt;

&lt;p&gt;These threads are pipelined, not synchronised step by step. While the Render thread is building frame N, the Game thread is usually already working on frame N+1, and the GPU may still be finishing frame N-1. That overlap is where most of the engine's throughput comes from, and it's also where most of the confusing bugs come from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fiig884bc1tcxw2ngko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fiig884bc1tcxw2ngko.png" alt="Game, Render and RHI thread pipelining" width="484" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The consequence is that the Game thread can't simply reach into the renderer and change things. Any data the renderer needs has to be handed over, and the Game thread has to keep going without waiting. Unreal does this in two ways.&lt;/p&gt;

&lt;p&gt;For components that draw something, the transfer is a&amp;nbsp;&lt;strong&gt;scene proxy&lt;/strong&gt;. A&amp;nbsp;&lt;code&gt;UPrimitiveComponent&lt;/code&gt;&amp;nbsp;creates a render-thread-owned copy of the state needed to draw it, generally a subclass of&amp;nbsp;&lt;code&gt;FPrimitiveSceneProxy&lt;/code&gt;. When the component's transform or render state changes, the engine pushes an update to that proxy instead of letting the renderer read the component directly. The renderer only ever sees proxies. That separation is why a component can be garbage-collected on the Game thread while its last frame is still being drawn.&lt;/p&gt;

&lt;p&gt;For everything else, there's&amp;nbsp;&lt;code&gt;ENQUEUE_RENDER_COMMAND&lt;/code&gt;, which queues a lambda to run on the Render thread at some later point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FLinearColor&lt;/span&gt; &lt;span class="n"&gt;Tint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ComputeTintOnGameThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;ENQUEUE_RENDER_COMMAND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateMyTint&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyProxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tint&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;FRHICommandListImmediate&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;RHICmdList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Runs on the Render thread, possibly a frame or more later.&lt;/span&gt;
        &lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetTint_RenderThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is the capture list. The lambda runs after the calling function has returned, so anything captured by reference is likely gone by then. Capturing by value, or moving data into the lambda, is what actually carries the state across the thread boundary. Sometimes if you are sure that a really large object wont be garbage collected or killed or accessed by the game thread, you can just pass it as reference inside the lambda. It's unsafe but we cut overhead of creating a copy of the object.&lt;/p&gt;

&lt;p&gt;This is also where the problem from the prologue came from.&amp;nbsp;&lt;code&gt;FlushRenderingCommands()&lt;/code&gt;&amp;nbsp;makes the Game thread wait until the Render thread has processed everything queued so far. It's a correct way to get a consistent view of render state, which is why AI generated code reaches for it. But it collapses the pipeline... for as long as the flush takes, the Game thread and the Render thread are no longer overlapping. That was the 7.5ms. It wasn't a bug in terms of output. It was a design flaw that was correct and slow at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Unreal is already a graphics library
&lt;/h2&gt;

&lt;p&gt;Unreal allows us to declare and do GPU calls which is similar to any low level graphics library but is abstracted by two layers. This is very similar to WGPU as Unreal's graphics library translates calls into native low-level GPU drivers like Vulkan, Metal, DirectX 12, or WEB GPU (some people have already done it!). &lt;/p&gt;

&lt;p&gt;Once the data reaches the Render thread, it goes through two abstraction layers before it becomes GPU work. &lt;/p&gt;

&lt;p&gt;The lower one is the&amp;nbsp;&lt;strong&gt;RHI&lt;/strong&gt;, the Render Hardware Interface. It's Unreal's portable graphics API... textures, buffers, shaders, pipeline state, draw and dispatch calls, expressed once and implemented per backend. Code talks to the RHI mostly through command lists (&lt;code&gt;FRHICommandList&lt;/code&gt;), which record commands that the RHI thread later translates into native API calls. Each RHI also reports a feature level, such as SM5, SM6 or ES3_1 for mobile, which decides what the hardware can be asked to do. That matters a lot once compute shaders enter the picture.&lt;/p&gt;

&lt;p&gt;Working directly at the RHI level feels familiar if you come from Vulkan or D3D12, and it comes with the same responsibilities. You create the resources, you track their lifetimes, and you issue the transitions that tell the GPU a texture is moving from "being written" to "being read."&lt;/p&gt;

&lt;p&gt;The layer above it is the&amp;nbsp;&lt;strong&gt;Render Dependency Graph (RDG)&lt;/strong&gt;, and this is where most modern rendering code in Unreal actually lives. Instead of issuing commands immediately, you describe a frame's worth of work as a graph of passes. Each pass declares, through its parameter struct, which resources it reads and which it writes.&lt;/p&gt;

&lt;p&gt;So the stack looks like this&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai2pttkq3sy2l0zhb6gi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai2pttkq3sy2l0zhb6gi.png" alt="Unreal render stack" width="800" height="1096"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the realisation I mentioned in the prologue. The question isn't whether to use a graphics library in Unreal. The RHI and RDG already are one. The work is learning its rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why all this matters?
&lt;/h2&gt;

&lt;p&gt;This chapter didn't add a single line of shader code, but it changed how I and you will read the problems that came later. "My shader doesn't run" stopped being one problem and became several... the shader wasn't found, the pass was culled or the resource was never transitioned, or maybe the result was read before the GPU produced it. Each of those lives in a different layer, and knowing the layers is what makes it possible to tell them apart.&lt;/p&gt;

&lt;p&gt;It also changed how I use AI-generated rendering code. The model can write a pass quickly. What I have is is the ability to ask which thread that pass runs on, what it waits for, and whether the profile agrees with what the ai generated code claims to do.&lt;/p&gt;

&lt;p&gt;The next chapter uses this world to read an existing plugin... &lt;code&gt;GPUCompositionGraphicsPipeline&lt;/code&gt;, which composites a media texture into the rendered frame. Where does it enter the frame, which thread does each part run on, and why is the work declared as passes instead of issued as draw calls?&lt;/p&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>gpu</category>
      <category>graphics</category>
    </item>
    <item>
      <title>Unreal Engine GPU Stack: Prologue</title>
      <dc:creator>Saish</dc:creator>
      <pubDate>Sun, 11 Oct 2026 20:57:48 +0000</pubDate>
      <link>https://dev.to/saish_in3d/unreal-engine-gpu-stack-prologue-59ck</link>
      <guid>https://dev.to/saish_in3d/unreal-engine-gpu-stack-prologue-59ck</guid>
      <description>&lt;h2&gt;
  
  
  1. First contact: The frame that fell apart
&lt;/h2&gt;

&lt;p&gt;The problem that started this for me was simple. A couple of years ago, I was required to process a lot of data every frame. There was a specific algorithm that I wanted to run on every pixel of each video frame the system received, within a certain timeframe, let's say 20ms.&lt;/p&gt;

&lt;p&gt;The obvious approach was the one every Unreal Engine developer reaches for first: loop over the frame's pixel array in &lt;code&gt;Tick&lt;/code&gt;, do the calculations, move on. At a few thousand pixels, it was fine. At a few hundred thousand, it was noticeable. Pushing toward 8.3 million pixels, the frame time fell apart. Multithreading could only help so much.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;stat unit&lt;/code&gt; made the problem clear enough. The Game thread was the one blowing the budget, while the GPU was comfortably idle. Unreal Insights confirmed it.. the time was spent in my update loop, doing the same small piece of math for every pixel, one after another.&lt;/p&gt;

&lt;p&gt;That's a workload the GPU is built for. So I moved the update into a compute shader, kept the data on the GPU, and profiled again.&lt;/p&gt;

&lt;p&gt;The result is the kind of before-and-after (&lt;strong&gt;7 FPS to 65 FPS on an RTX 3080&lt;/strong&gt;) that makes GPU programming look like magic. It isn't. Getting there raised more questions than it answered. Why exactly was it faster? What was Unreal doing with my shader between dispatch and screen? Searching for answers to these questions led me down a deeper rabbit hole into Unreal Engine's GPU side.&lt;/p&gt;

&lt;p&gt;That rabbit hole meant hours of deep dives into Unreal Engine source code, since content on this topic is very scarce on the web. I had to go on many side quests, digging into much lower-level graphics APIs like DirectX, OpenGL and sometimes Vulkan. Many concepts from CUDA translated very well while writing compute shaders. Random Chinese blog posts proved to be really good reference points in this odyssey.&lt;/p&gt;

&lt;p&gt;Even now, there is still no single good source of information that answers all the questions I had a few years ago.&lt;/p&gt;

&lt;p&gt;This series is my attempt to answer those questions properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why the GPU stack and not graphics programming?
&lt;/h2&gt;

&lt;p&gt;Graphics programming is about producing images. The GPU stack covers everything that runs on the GPU: compute, data movement, interop and inference. Generating synthetic sensor data is a good example. It needs physics calculations on the GPU that read vertex buffers already living there, plus uploads and readbacks that stay in sync with the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Graphics and AI are converging
&lt;/h2&gt;

&lt;p&gt;The GPU in a modern game isn't only rendering triangles anymore. Neural rendering is already shipping: DLSS 5, XeSS and newer versions of FSR use neural networks for upscaling, frame generation reconstructs whole frames, and ray reconstruction and neural denoising replace hand-tuned filters. Behind those, research is moving quickly toward neural materials, neural texture compression, Gaussian splatting, learned radiance caches, world models, and ML-driven animation and physics.&lt;/p&gt;

&lt;p&gt;What surprised me was how much of this ends up running on the same GPU, using the same memory and fighting for the same frame budget as the renderer. Once you look past the terminology, the overlap is large. A tensor is a buffer. Running inference is dispatching compute work. Moving data between CPU and GPU costs the same whether it's vertex positions or model weights. The engineer who understands how a frame moves through the GPU is in a good position for both real-time graphics and on-device AI, which is why this series includes a chapter on inference inside the engine rather than treating it as a separate topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Who is this for?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The same problem looks different depending on where you come from.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Who hits this wall, and how, varies a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a gameplay programmer&lt;/strong&gt;, it usually shows up as a system that stops scaling. Crowds, projectiles, destruction or simulation work fine until the object count grows, and then &lt;code&gt;Tick&lt;/code&gt; and Blueprint loops become the bottleneck. The hard part isn't writing a shader. It's moving the system to the GPU without breaking the gameplay logic that depends on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a general software engineer&lt;/strong&gt;, you might be looking to build a non-gaming software extension or layer with Unreal. The GPU is best understood as a massively parallel processor. The ideas are the same as in CUDA or an ML framework.. batch the work, keep data on the device, avoid round trips. What's different in a game engine is the strict frame budget and the fact that you share the GPU with a renderer that has its own opinions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a technical artist&lt;/strong&gt;, the wall is the edge of the material editor. You can see why an effect is expensive but not what's happening beneath the nodes, and some ideas simply can't be expressed as a graph. You might find answers to questions like: Why does adding a Scene Capture Component to the level eat up so much FPS? How can I optimize my work? The short answer is that a scene capture renders the scene again from another view, so all the rendering pipeline calculations and dependencies have to be redone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And if you're a graphics programmer&lt;/strong&gt; coming from Vulkan, DirectX 12 or OpenGL, you already understand the GPU. The challenge is mapping that knowledge onto Unreal's abstractions, which don't always look like the APIs you know.&lt;/p&gt;

&lt;p&gt;This series tries to serve all four, starting from the problem rather than from the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Vibe coding without understanding internals
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1arwytr56rxh8c42i44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1arwytr56rxh8c42i44.png" alt=" " width="588" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb771pwe90zdi6sw5hh0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb771pwe90zdi6sw5hh0m.png" alt=" " width="585" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI assistants can produce a compute shader, a render pass or a Scene View Extension in seconds. That's genuinely useful, and I started to use them recently after a bit of reluctance. But GPU code in Unreal is an especially easy place for generated code to go wrong. Recently, while building a GPU-accelerated data processing pipeline, I prompted Claude Fable with a good amount of context to build the system. It wrote the code for me, the code compiled, and everything worked fine. But when I profiled the system, I found that a specific part of it was taking &lt;strong&gt;7.5ms by blocking the game thread&lt;/strong&gt;. After searching through a few hundred lines of code, I found a particular line where &lt;code&gt;FlushRenderingCommands()&lt;/code&gt; was used, which was unnecessary for this design. &lt;code&gt;FRHIGPUFence&lt;/code&gt; would have worked better (I'll explain why in the following chapters). With a simple prompt to rework the system to use &lt;code&gt;FRHIGPUFence&lt;/code&gt; properly instead of &lt;code&gt;FlushRenderingCommands()&lt;/code&gt;, I was able to cut the processing time on the game thread for that system from &lt;strong&gt;7.5ms to 2.3ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Part of the reason is specific to Unreal.. its rendering APIs change between 5.x releases. Functions get renamed, deprecated or moved, and the model's training data contains all of those versions at once. The generated code often mixes an older pattern with a newer one, and the result may or may not compile.&lt;/p&gt;

&lt;p&gt;The more dangerous case is when it does compile. &lt;strong&gt;Code that compiles is not the same as code that works.&lt;/strong&gt; A render pass can build cleanly and output a black screen. A readback can work perfectly and quietly stall the frame. Each of those looks like one symptom but has a different root cause, and without a mental model of the layers underneath, you can't tell which one you're looking at. Worse, you can't tell a correct answer from a confident wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The OpenGL vs Unreal beginner's dilemma
&lt;/h2&gt;

&lt;p&gt;Experts often suggest going with OpenGL as your first graphics API to learn the concepts. While cross-referencing a lot of rendering content from OpenGL, I had a big decision to make. As a beginner, I found that OpenGL had easily available learning resources, while Unreal's were scarce and the learning curve was steep. As a person with a toxic habit of doing things the difficult way, I chose Unreal, and after a few months, I had a realization.&lt;/p&gt;

&lt;p&gt;The realization that changed my approach was that &lt;strong&gt;Unreal already is a graphics library&lt;/strong&gt;. The RHI (Render Hardware Interface) is an abstraction over D3D12, Vulkan and Metal. The Render Dependency Graph (RDG) sits on top of it and manages resources and passes. The question isn't whether to use a graphics library. It's whether to learn the one already shipping in the engine.&lt;/p&gt;

&lt;p&gt;If you already know Unreal Engine, go with it as your graphics library using this series as learning material, since it's easier to build games and projects when the rest of the tooling is already available. If you're starting from the beginning without any game engine experience, I'd probably start with OpenGL.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What this series covers
&lt;/h2&gt;

&lt;p&gt;Each chapter starts from a problem and builds a solution on the GPU. The chapters roughly follow the order in which I had to learn things, from the fundamentals of how Unreal renders a frame, to running neural networks inside it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GPU world building and context setup.&lt;/strong&gt; The foundations the rest of the series builds on: rendering fundamentals, the Game, Render and RHI threads, and what RHI and RDG actually do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scene View Extensions.&lt;/strong&gt; How to inject your own rendering pass into Unreal's frame without modifying the engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute shaders.&lt;/strong&gt; Writing a shader in a plugin, dispatching it through the Render Dependency Graph, and running general-purpose work on the GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU-GPU data upload and readback.&lt;/strong&gt; Getting data onto the GPU and results back, and why a readback that works can still stall the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combining compute, vertex and pixel shaders for particle effects.&lt;/strong&gt; Simulating on the GPU with compute and drawing the results without ever copying them back to the CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU mesh and a custom mesh pipeline.&lt;/strong&gt; How Unreal actually draws a mesh, through scene proxies, vertex factories and mesh batches, and how to build your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU vertex animation.&lt;/strong&gt; Animating thousands of meshes by doing the work in the vertex pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU physics.&lt;/strong&gt; Running simulation on the GPU, and the problems that appear when gameplay needs to know the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-accelerated cross-API interoperability.&lt;/strong&gt; Sharing GPU memory between Unreal and other APIs, using video streaming as the example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-accelerated AI inference.&lt;/strong&gt; Running neural networks inside the engine, on the same GPU and within the same frame budget as the renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profiling and debugging.&lt;/strong&gt; Unreal Insights, GPU profiling, RenderDoc and RDG's debugging tools, and how to tell whether something is actually faster and why, or why it isn't running at all. It's listed last, but it's worth reading whenever the numbers stop making sense.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Epilogue: What's next.&lt;/strong&gt; Where this leads, what's possible, and projects and examples worth studying.&lt;/p&gt;




&lt;p&gt;If you have questions along the way, send them. They'll shape the FAQ at the end of future chapters.&lt;/p&gt;

</description>
      <category>unrealengine</category>
      <category>gamedev</category>
      <category>gpu</category>
      <category>graphics</category>
    </item>
  </channel>
</rss>
