Featured image of post Unreal Performance Maxxing

Unreal Performance Maxxing

With the DirectX Mobile Renderer on Desktop.

Difficulty | Master ⚖️

This guide aims to explain a destructive method to optimize Unreal Engine to achieve high performance for both CPU and GPU by sacrificing rendering features.

Required Software:

  • RenderDoc
  • Source Build of Unreal Engine (Only needed for Profiling)

Ahead of time you should be warned that you will lose a huge amount of modern rendering features, and if you follow this guide exactly, you will also lose notable features such as DirectX 12, Deferred Rendering, and SM6 support – however you can optionally take the performance hit and turn some of these features back on manually.

This method requires engine mods, however it does not require building Unreal from source, we will instead use an installed build from the Epic Games Launcher and focus on a method which includes hotpatching the launcher version. It’s worth noting that it is possible to go even further with proper modifications to the engine, and that in a project that is going to ship properly, you’d likely want to integrate the changes into the engine directly instead of just hotpatching. We will however need to build from source once, just to verify everything is working if you do this yourself, because you cannot properly profile this without a source build.

If you can’t be bothered to follow this guide yourself to understand how it works, there is now also a mobile branch type on the Daft Starter Project which will get you started with all of the things you need.

Valorant

First things first, the idea to do this came primarily from Valorant, while it is not the exact same method, it’s somewhat similar in the outcomes - notably they built their own forward renderer from a forked version of the legacy mobile renderer, we won’t be doing this, but we will be achieving the same end result. A few things have changed since then, primarily that the support for rendering as a mobile device in the editor has been vastly improved, and the forward renderer was ruthlessly battle tested with VR support for Robo Recall meaning we have many nice features that Valorant could simply not take advantage of back in the day. Also worth noting that on lower end devices and some console devices like the switch, Epic also is abusing the DirectX mobile renderer, so the strategy Valorant used has become a lot more viable and supported - big thanks to Marcus Reid (Riot Nu) and Riot Games for sharing how they got Unreal running so well.

Disabling Rendering Features

While you don’t have to disable all of these features, it’s worth noting that some of them are going to be disabled anyway and we are going to assume for the sake of this guide that you want the renderer near as light as possible (obviously you can go further if you need). One of the primary benefits of just disabling the features is for permutation reduction of disabling parts of the renderer so you aren’t paying for them if you don’t use them. I recommend to change the following settings in DefaultEngine.ini under Project/Config.

When using the settings below, I can recommend understand what each one does, what you are losing and if each one is a benefit to your project, I do not claim that these settings are uniformally faster, it ALWAYS depends per project especially with projects closer to shipping, best policy is to profile them! Also your artists are going to be mad at losing some of these features so probably order a riot shield or such if possible before enabling these settings at once without consulting your artist.

Renderer Settings - Disable / Enable Rendering Features.

[/Script/Engine.RendererSettings]
r.AllowStaticLighting=False # Disables lightmass related rendering code.
r.GenerateMeshDistanceFields=True # We still want distance field rendering as it's typical to use.
r.SkinCache.CompileShaders=True
r.RayTracing=False # Ray Tracing is too expensive to run on low end devices with no RTCores or support for HWRT.
r.Shadow.Virtual.Enable=0 # Virtual Shadow maps are too expensive without nanite support, CSM will suffice.
r.Nanite=0 # Nanite is unavailable on older GPU feature sets or slow without fast compute.
r.Nanite.ProjectEnabled=False
r.PathTracing=False # Disable path tracing render pathways
r.CustomDepth=3 # You can disable this to save VRAM and perf writing stencil buffers.
r.DefaultFeature.AutoExposure=False # Auto exposure isn't much use without physically based lighting, which we likely won't use.
r.AntiAliasingMethod=3 # Oldschool FXAA, no TAA or TSR because it's expensive, you can go with MSAA if you want nice AA.
r.MSAACount=1
r.SupportLocalFogVolumes=False # Disable fog volume permutations.
r.HeterogeneousVolumes=False # Disable expensive volumetric rendering pathways.
r.DefaultFeature.MotionBlur=False # Motion blur adds cost from velocity and post process, and nobody likes it anyway.
r.Lumen.Reflections.HardwareRayTracing.Translucent.Refraction.EnableForProject=False # Disable HWRT.
r.VirtualTextures=False # You can use virtual texturing if you need, but generally it's not needed by default.
r.VT.AnisotropicFiltering=False # Disable Anisotropic Filtering for VTs which has some overhead, which is off anyways.
bEnableVirtualTexturePostProcessing=False # Don't need.
r.DefaultFeature.Bloom=False # Bloom has a little bit of overhead from blurring low mips.
r.Substrate=false # Substrate has high overhead compared to legacy material system, no benefit here.
r.SupportSkyAtmosphere=False # Reduce material permutations, less PSOs & compile time.
r.ForwardShading=True # Very important - Forward shading has considerable perf benefits, but it can vary per scene \ game, so profile it.
r.SupportStationarySkylight=False # Reduce material permutations, less PSOs & compile time.
r.SupportPointLightWholeSceneShadows=False # Reduce material permutations, less PSOs & compile time.
r.SupportLowQualityLightmaps=False
r.Mobile.EnableStaticAndCSMShadowReceivers=False # Reduce material permutations, less PSOs & compile time.
r.Mobile.AllowDistanceFieldShadows=False # You may want this on since we are paying for DFs anyway, can be cheap soft shadows vs CSM.
r.DBuffer=False # Remove Decal Buffer for prepass, makes decals more expensive but removes overall overhead.
r.DefaultFeature.AmbientOcclusion=False # AO looks pretty so some projects may want it - you can use DFAO, but we min max perf here.
r.DefaultFeature.AmbientOcclusionStaticFraction=False
r.EarlyZPass=0 # Remove Depth Prepass, this has considerable impact on overdraw so be careful, measure for your scene.
r.Refraction.Blur=False
r.AllowOcclusionQueries=False # Disable Occlusion Culling - it's expensive, measure for your scene, some complex scenes will benefit.
r.TextureStreaming=False # No texture streaming, just blast mip0.
r.Mobile.Forward.EnableLocalLights=0 # We don't need local lights, more perf plz.
r.Mobile.EnableMovableLightCSMShaderCulling=False
r.Mobile.SupportsGen4TAA=False # We will not be using TAA
r.Mobile.AntiAliasing=0 # Disable AA by default, but enable this for FXAA or MSAA.

RHI Settings - Enable DX11 + SM5 & ES31 and disable DX12 + SM6

[/Script/WindowsTargetPlatform.WindowsTargetSettings]
DefaultGraphicsRHI=DefaultGraphicsRHI_DX11
-D3D12TargetedShaderFormats=PCD3D_SM5
-D3D11TargetedShaderFormats=PCD3D_SM5
+D3D11TargetedShaderFormats=PCD3D_SM5
+D3D11TargetedShaderFormats=PCD3D_ES31

Something of note here - you may be confused why we didn’t just disable SM5 and fully send it with ES31, the reason is because without Engine mods it’s not possible and the editor requires feature level 5 to launch, you will get an error on startup, however we will be using ES31 shader format primarily.

You can read the ini for an idea of all the features you are opting out of here - I recommend cherry picking them, but as a rough outline by straight lifting these settings you lose:

  • VSM
  • Nanite
  • Z Prepass
  • DBuffer
  • Ambient Occlusion
  • Substrate
  • Deferred Rendering
  • Virtual Texturing
  • Static Lighting
  • Ray Tracing & HWRT
  • Path Tracing
  • TAA and TSR
  • Lighting based shader permutations
  • Texture Streaming
  • Sky Atmosphere
  • Bloom

How do I know which features I can or can’t use??? Luckily - Epic makes a handy list on this documentation to tell you exactly which ones you can or can’t.

DirectX Mobile

So great, now we have the rendering settings we need, what now?? Well we need to first enable DirectX Mobile mode in our packaged game. We do this by specifying the preferred RHI. To do this we need to create a new file under Project/Config named “DefaultGameUserSettings.ini” and then add the following lines to the file and save it.

[D3DRHIPreference]
bPreferFeatureLevelES31=True

This setting tells Unreal that it should initialize using the Mobile Renderer on DirectX by forcing the feature level to ES31 instead of the SM5 renderer. You can confirm this is working by looking at your logs.

Forcing Mobile Preview in Editor

Source/StarterHotpatch/StarterHotpatch.Build.cs

using UnrealBuildTool;

public class StarterHotpatch : ModuleRules
{
	public StarterHotpatch(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PrivateDependencyModuleNames.AddRange(new string[]
		{
			"Core",
			"Engine",
			"UnrealEd"
		});
	}
}

Add the following to your uproject - it is extremely important you set the loading phase to EarliestPossible otherwise the hook will happen after the renderer initializes and patch too late. You must also make this an editor only module, because we only care about patching when launching the actual editor, we shouldn’t do this on a packaged game.

StarterProject/Starter.uproject

	"Modules": [
		{
			"Name": "StarterRuntime",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		},
		{
			"Name": "StarterEditor",
			"Type": "Editor",
			"LoadingPhase": "Default"
		},
		{
			"Name": "StarterHotpatch",
			"Type": "Editor",
			"LoadingPhase": "EarliestPossible"
		}
	],

Source/StarterHotpatch/StarterHotpatchModule.cpp

class FStarterHotpatchModule : public IModuleInterface
{
    virtual void StartupModule() override
    {
        // Hook to map changes to toggle back to the mobile preview.
        FEditorDelegates::MapChange.AddRaw(this, &FStarterHotpatchModule::OnMapChange);
    }

    virtual void ShutdownModule() override
    {
        FEditorDelegates::MapChange.RemoveAll(this);
    }

    void OnMapChange(uint32)
    {
        check(GEditor);

        // Force the editor into DirectX Mobile Preview on ES31
        // Stole these settings from Engine/Config/Windows/DataDrivenPlatformInfo.ini
        const FPreviewPlatformInfo PreviewPlatform(
            ERHIFeatureLevel::ES3_1,
            EShaderPlatform::SP_PCD3D_ES3_1,
            FName("PC"),
            FName("PCD3D_ES31"),
            FName("Windows_Preview_ES31"),
            true,
            FName("PCD3D_ES3_1_Preview")
            );
        
        GEditor->SetPreviewPlatform(PreviewPlatform, false);
    }
};

IMPLEMENT_MODULE(FStarterHotpatchModule, StarterHotpatch);

Now you will notice whenever the editor loads or changes maps, you will automatically be swapped back to mobile preview no matter what. The editor does defaultly save your preview settings, but what it won’t do is force a new user loading up the project into that mode directly, if this is working properly you should see in the bottom right of the viewport “Preview Platform: PC D3D Mobile”. You can likely improve the user experience of this a bit, but I wanted to keep it simple for this guide.

Hotpatching Mobile Render Features

Ok so cool, we have the mobile preview working and our game runs in ES31 with D3D Mobile render. But wait, ES31 is missing features, What if you actually needed those?! Well you have basically 2 options here. Instead of using ES31 you can use Mobile Vulkan, this render pathway is the primary one used by the Oculus Quest, so it has reasonably good support and some beefier feature support at the cost of some perf, however for more complex modern rendering this may be more appropriate, I won’t be covering that here, but the way to set it up is reasonably similar.

The alternative is that we actually just hotpatch the features back into D3D Mobile. Something worth noting is that not all features are going to work or be supported by this renderer, it is fully trial and error and you are at mercy of what Epic is supporting on ES31, if they don’t support it, you will have to mod the engine to support it yourself.

Around 2019, Epic added support for “Platforms”, which is how you are able to extend the engine for a given OS like Playstation, Switch, Android and it just works despite the vast differences in architectures and features. Luckily all platforms are defined this way, and are split into “vanilla” and non-vanilla platforms, D3D Mobile is part of the Windows platform and all of the features it uses are defined in an ini file that exists in the engine, if one wanted to, we could just go modify that ini file (DataDrivenPlatformInfo.ini) to turn some features back on - and we will do this exactly by writing a hook on our project which patches this ini to turn features on.

For this example we are going to be enabling Signed Distance Fields (SDF) or “Distance Fields” which are by default turned off in the mobile renderer.

Source/StarterHotpatch/StarterHotpatchModule.cpp

/**
 * @HACK!!! BIG STINKY HACK. As of UE5.5 there's no project level extensions for platforms
 * read : https://forums.unrealengine.com/t/attention-platform-changes-ahead/126005
 *
 * What this means is that because we use ES31 feature level on this project, but we don't
 * use a custom engine (for now), we hotpatch the engine install to enable distance field
 * support for DirectX 11 Mobile support.
 *
 * This is done by essentially directly editing the engine ini which defines the platform
 * properties for windows, there is no consequence for doing so as there is actually an
 * editor preview for mobile which has SDF enabled, however for whatever reason they made
 * no actual shippable version of this when setting prefer RHI to ES31.
 */
class FStarterHotpatchModule : public IModuleInterface
{
    virtual void StartupModule() override
    {
        // Hotpatch Engine/Config/Windows/DataDrivenPlatformInfo.ini, we find section
        // [ShaderPlatform PCD3D_ES3_1] and then look for bSupportsDistanceFields = false
        // and patch the value onto true, otherwise our lighting will be black due to no mobile DF support.
        FString IniPath = FPaths::Combine(FPaths::EngineDir(), TEXT("Config/Windows/DataDrivenPlatformInfo.ini"));
        FString FileContent;

        if (FPaths::FileExists(IniPath) && FFileHelper::LoadFileToString(FileContent, *IniPath))
        {
            const FString SectionHeader = TEXT("[ShaderPlatform PCD3D_ES3_1]");
            const FString Key = TEXT("bSupportsDistanceFields");
            const FString OldLine = Key + TEXT(" = false");
            const FString NewLine = Key + TEXT(" = true");

            int32 SectionStart = FileContent.Find(SectionHeader, ESearchCase::IgnoreCase);
            if (SectionStart != INDEX_NONE)
            {
                int32 KeyStart = FileContent.Find(OldLine, ESearchCase::IgnoreCase, ESearchDir::FromStart, SectionStart);
                if (KeyStart != INDEX_NONE)
                {
                    FileContent = FileContent.Replace(*OldLine, *NewLine, ESearchCase::IgnoreCase);
                    FFileHelper::SaveStringToFile(FileContent, *IniPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
                }
            }
        }
    }
};

Now whenever the editor is booted up we automatically hotpatch bSupportsDistanceFields to true inside /Engine/Config/Windows/DataDrivenPlatformInfo.ini which will make the engine use distance field supports whenever using the PCD3D_ES1_1 shader platform. You can undo this engine patch by either repairing your engine install, or you could write some additional code to “unpatch” the same ini.

Testing it works

First of all, do not test this in the editor or in development config - neither of these are good enough to be verifying or testing performance as they are not representative of your real game perf. To verify and profile your game performance you should always use either Test config or Shipping.

To profile we add the following into our Game Target.cs, this must be the GAME target, not the editor target - identify this by the once with Type = TargetType.Game, if you have multiple game targets, identify the one you will be packaging and use that.

bAllowProfileGPUInTest = true;
bUseConsoleInShipping = true;
bUseExecCommandsInShipping = true;
GlobalDefinitions.Add("FORCE_USE_STATS=1");

Once you have these settings in your target, go ahead and package the game in Test configuration. When we run this exe we will now have profiling information available if we were to launch insights and more importantly RenderDoc will now give us debug information about the RHI so we can actually see what things are. When the work we did above is working, you’re going to see a pretty big speedup in stat unit

Ok so how much faster is it?

That really depends on your scene. However you can expect even an empty scene to have significant improvements in FPS, you can see the before (stock 5.5 engine settings on the left) and the mobile renderer on the right in an empty scene just for a guide on how much overhead we have eliminated just from the base engine when it’s not even doing anything.

Empty scene comparison

While this performance delta may seem surprising, the left hand is just how unreal performs without touching any settings or performing any optimizations, the righthand is after some pretty aggressive feature stripping and the mobile rendering.

Full List of Platform Features

Here is the list of all the features that can possibly be enabled per platform on the renderer for your own trial and error, however none of these features are a given to work on ES31 or be supported for mobile, so you will just have to test it yourself, have fun.

bSupportsDebugViewShaders = false
bSupportsMobileMultiView = false
bSupportsArrayTextureCompression = false
bSupportsDistanceFields = false
bSupportsDiaphragmDOF = false
bSupportsRGBColorBuffer = false
bSupportsPercentageCloserShadows = false
bSupportsIndexBufferUAVs = false
bSupportsInstancedStereo = false
SupportsMultiViewport = 0
bSupportsMSAA = false
bSupports4ComponentUAVReadWrite = false
bSupportsShaderRootConstants = false
bSupportsShaderBundleDispatch = false
bSupportsRenderTargetWriteMask = false
bSupportsRayTracing = false
bSupportsRayTracingCallableShaders = false
bSupportsRayTracingProceduralPrimitive = false
bSupportsRayTracingTraversalStatistics = false
bSupportsRayTracingIndirectInstanceData = false
bSupportsHighEndRayTracingEffects = false
bSupportsPathTracing = false
bSupportsGPUScene = false
bSupportsUnrestrictedHalfFloatBuffers = false
bSupportsByteBufferComputeShaders = false
bSupportsPrimitiveShaders = false
bSupportsUInt64ImageAtomics = false
bRequiresVendorExtensionsForAtomics = false
bSupportsNanite = false
bSupportsLumenGI = false
bSupportsSSDIndirect = false
bSupportsTemporalHistoryUpscale = false
bSupportsRTIndexFromVS = false
bSupportsWaveOperations = 0
bSupportsWavePermute = false
MinimumWaveSize = 0
MaximumWaveSize = 0
bSupportsIntrinsicWaveOnce = false
bSupportsConservativeRasterization = false
bRequiresExplicit128bitRT = false
bSupportsGen5TemporalAA = false
bTargetsTiledGPU = false
bNeedsOfflineCompiler = false
bSupportsComputeFramework = false
bSupportsAnisotropicMaterials = false
bSupportsDualSourceBlending = false
bRequiresGeneratePrevTransformBuffer = false
bRequiresRenderTargetDuringRaster = false
bRequiresDisableForwardLocalLights = false
bCompileSignalProcessingPipeline = false
bSupportsMeshShadersTier0 = false
bSupportsMeshShadersTier1 = false
bSupportsMeshShadersWithClipDistance = false
MaxMeshShaderThreadGroupSize = 0
bRequiresUnwrappedMeshShaderArgs = false
bSupportsPerPixelDBufferMask = false
bIsHlslcc = false
bSupportsDxc = false
bIsSPIRV = false
bSupportsVariableRateShading = false
NumberOfComputeThreads = 0
bWaterUsesSimpleForwardShading = false
bSupportsHairStrandGeometry = false
bSupportsDOFHybridScattering = false
bNeedsExtraMobileFrames = false
bSupportsHZBOcclusion = false
bSupportsWaterIndirectDraw = false
bSupportsAsyncPipelineCompilation = false
bSupportsVertexShaderSRVs = false
bSupportsVertexShaderUAVs = 0
bSupportsManualVertexFetch = false
bRequiresReverseCullingOnMobile = false
bOverrideFMaterial_NeedsGBufferEnabled = false
bSupportsFFTBloom = false
bSupportsInlineRayTracing = false
bSupportsRayTracingShaders = false
bSupportsVertexShaderLayer = false
BindlessSupport = 0
StaticShaderBindingLayoutSupport = 0
bSupportsVolumeTextureAtomics = false
bSupportsROV = false
bSupportsOIT = false
bSupportsRealTypes = 0
EnablesHLSL2021ByDefault = 0
bSupportsSceneDataCompressedTransforms = false
bIsPreviewPlatform = false
bSupportsSwapchainUAVs = false
bSupportsClipDistance = false
bSupportsNNEShaders = false
bSupportsShaderPipelines = false
bSupportsUniformBufferObjects = false
bRequiresBindfulUtilityShaders = false
MaxSamplers = 0
SupportsBarycentricsIntrinsics = false
SupportsBarycentricsSemantic = 0
bSupportsWave64 = false
bSupportsIndependentSamplers = false
bSupportsWorkGraphs = false

Special Thanks

  • 0lento
  • Zeblote
  • Riot Nu
Built with Hugo
Theme Stack designed by Jimmy