I ran into a CEF initialization issue while integrating a bundled/custom CEF runtime into an Unreal Engine plugin.
The confusing part: everything worked in PIE / editor play, but failed in Play Standalone.
Symptoms
In Standalone, CEF failed before any rendering or page loading happened:
CefInitialize result: false
CefBrowserHost::CreateBrowserSync result=null
CreateBrowserSync returned null — CEF may not be initialized or the subprocess is missing.
This happened even though the obvious paths were valid:
browser_subprocess_path exists=true
resources_dir_path exists=true
locales_dir_path exists=true
libcef.dll exists=true
So the issue was not the render handler, texture upload, URL, JavaScript bridge, or UMG/material setup. The browser was never created.
Root cause
The problem was a shared CEF cache/profile path.
The Unreal Editor process and the Standalone game process were both using the same CEF profile/cache directory. CEF uses a process/profile singleton internally, so the second process can get redirected/refused by the already-running profile owner.
In practice, both processes were using something like:
.../SWUI_BundledCEF/Default
CEF logs may show clues such as:
Please customize CefSettings.root_cache_path for your application.
Handling STARTUP request from another process
Opening in existing browser session
The result:
shared CEF cache/profile
→ CEF process singleton conflict
→ CefInitialize returns false
→ CreateBrowserSync returns null
→ white screen / no browser
Fix
Give each Unreal process its own CEF root/cache/log path. Using the process ID is a simple and reliable approach.
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "Misc/Paths.h"
static void SetCefString(cef_string_t& Target, const FString& Value)
{
const FTCHARToUTF8 Utf8Value(*Value);
CefString(&Target).FromString(Utf8Value.Get(), Utf8Value.Length());
}
Then configure CEF like this before calling CefInitialize:
const uint32 ProcessId = FPlatformProcess::GetCurrentProcessId();
const FString RootCachePath = FPaths::ConvertRelativePathToFull(
FPaths::Combine(
FPlatformProcess::UserTempDir(),
FString::Printf(TEXT("MyPlugin_CEF_%u"), ProcessId)
)
);
const FString CachePath = FPaths::Combine(RootCachePath, TEXT("Default"));
const FString CefLogPath = FPaths::ConvertRelativePathToFull(
FPaths::Combine(
FPaths::ProjectLogDir(),
FString::Printf(TEXT("MyPlugin_CEF_%u.log"), ProcessId)
)
);
IFileManager::Get().MakeDirectory(*RootCachePath, true);
IFileManager::Get().MakeDirectory(*CachePath, true);
SetCefString(CefSettings.root_cache_path, RootCachePath);
SetCefString(CefSettings.cache_path, CachePath);
SetCefString(CefSettings.log_file, CefLogPath);
Also make sure CEF subprocess command-line arguments are not disabled:
CefSettings.command_line_args_disabled = 0;
CEF subprocesses need arguments such as:
--type=renderer
--type=gpu-process
Result
Instead of multiple Unreal processes sharing one CEF profile:
.../MyPlugin_CEF/Default
each process gets its own:
.../MyPlugin_CEF_8872/Default
.../MyPlugin_CEF_14868/Default
After this change, Standalone initialized correctly:
CefInitialize result: true
CreateBrowserSync result=valid
Debugging tip
If you are debugging CEF inside Unreal, first check whether CefInitialize actually succeeds. If it returns false, do not waste time debugging rendering, texture upload, URLs, or JavaScript integration yet.
The browser does not exist until initialization succeeds.
On Windows, you can also check which CEF DLLs are loaded into the Unreal process:
Get-Process UnrealEditor | ForEach-Object {
$p=$_.Id
Get-Process -Id $p -Module -ErrorAction SilentlyContinue |
Where-Object { $_.ModuleName -match "libcef|chrome_elf" } |
Select-Object @{n="PID";e={$p}},ModuleName,FileName
}
This is especially useful if your plugin ships its own CEF while Unreal’s own WebBrowser/Fab/editor features may also load a CEF runtime.