NixOS FHS flake/develop env for Unreal Engine.
- You must have access to the Unreal Engine source (via Perforce depot or Epic GitHub)
- Pull from your Perforce depot, or
git clone git@github.com:EpicGames/UnrealEngine.git --branch release --single-branch - Inside the
UE/folder, rungit clone git@github.com:Yagi-Michael/NixOSUnreal.git flake cd flake && nix developunreal-fhscd ..- Note: Perforce doesn't preserve Unix execute permissions. You may need to run:
chmod +x Setup.sh GenerateProjectFiles.sh Engine/Binaries/DotNET/GitDependencies/linux-x64/GitDependencies
- Note: Perforce doesn't preserve Unix execute permissions. You may need to run:
./Setup.sh./GenerateProjectFiles.sh- Note: You may need to fix permissions on engine build scripts first:
find Engine/Build -name "*.sh" -exec chmod +x {} +
- Note: You may need to fix permissions on engine build scripts first:
make -j1we must use-j1- Once compiled you will find the binary at
Engine/Binaries/Linux/UnrealEditor
UE
├── <YourProject> (game project)
├── Engine (engine source)
├── flake <---| Put flake here
├── Setup.sh
└── GenerateProjectFiles.sh
cd UE/flake && nix develop # enter nix shell (sets up PATH, env vars)
unreal-fhs # enter FHS sandbox (GPU, Vulkan, libs)
run-unreal # launch UnrealEditor (auto-detects Vulkan/OpenGL)| Command | Description |
|---|---|
unreal-fhs |
Enter the FHS environment with all system deps |
run-unreal [--x11] [path] [args] |
Launch UnrealEditor (defaults to ../Engine/Binaries/Linux/UnrealEditor) with native Wayland by default; --x11 selects the fallback |
run-rider [--x11] |
Launch Rider IDE inside the FHS environment; UE spawned from Rider uses native Wayland by default, with --x11 as fallback |
vulkan-test |
Quick Vulkan sanity check |
vulkan-diag |
Detailed Vulkan diagnostics |
kill-unreal |
Kill running Unreal processes |
kde-wayland-settings |
Wayland keyboard input workaround info |
refresh-env |
Re-detect GPU and refresh environment variables |
toggle-engine-debug-symbols |
Move engine .debug files in/out of debug-stash/ to control LLDB memory usage |
gen-compile-commands |
Generate compile_commands.json from UBT for clangd intellisense |
run-zed |
Launch Zed editor inside the FHS environment |
build-ascent-test [--clean] [--print-only] |
Build staged Linux Test Game + Server artifacts with manifest/log provenance |
run-ascent-test <mode> |
Run a staged Test artifact — see below |
run-ascent-gauntlet [--build DIR] [--print-only] |
Run the packaged Gauntlet multiplayer race against a staged Test build |
check-ascent-tu <file>... |
Verify files compile using real UBT flags — authoritative answer for agents; see docs/superpowers/specs/2026-08-06-agent-compile-verification-design.md |
run-unreal-insights [args] |
Launch the Unreal Insights trace viewer |
run-trace-server [fork|kill] |
Start/stop the background trace store server (TCP 1981/1989) |
Four modes, all operating on Artifacts/Tests/latest:
| Mode | Purpose |
|---|---|
smoke |
Bounded headless boot/log/crash gate; writes runs/<id>-smoke/result.json |
scenario <name> [--timeout N] |
Deterministic scripted race with evidence capture |
client |
Launch the packaged Test client for human feel testing |
server |
Launch the packaged Test dedicated server |
Scenario mode is rendered and offscreen (-RenderOffscreen at 1280x720) because it captures
PNG evidence — it is not -nullrhi. It takes a fixed argument set and does not accept
free-form UE arguments; the exact command used is recorded in each run's command.txt.
build-ascent-test requires a reachable Perforce server (it records p4 opened in the
manifest) and will exit if P4 is unavailable. Use --clean after changing a USTRUCT
property layout: cooked assets use unversioned property serialization, so an iterative cook
can leave cooked data whose layout no longer matches the compiled binary, which crashes at
startup in SerializeUnversionedProperties.
Each immutable artifact now archives both targets under one staged root instead of separate
client/ and server/ roots:
Artifacts/Tests/<build-id>/
├── staged/
│ ├── Linux/ # Test client
│ └── LinuxServer/ # Test dedicated server
├── manifest.json
├── build-client.log
└── build-server.log
Artifacts/Tests/latest points to the newest successful immutable artifact.
The FHS env uses native Wayland by default — verified daily-drivable on Plasma 6 / KWin
(UE 5.7.4, SDL3 3.2.10). Use --x11 only for the explicit X11 fallback:
run-unreal ../Engine/Binaries/Linux/UnrealEditor /path/to/Project.uproject
run-rider # UE launched from Rider run configs inherits the Wayland env
run-unreal --x11 # X11 fallback
run-rider --x11 # UE launched from Rider run configs inherits the X11 envWhat the flake handles (modules/core.nix, UE_X11=0 default branch):
- Sets
SDL_VIDEODRIVER=wayland+SDL_VIDEO_DRIVER=wayland, skips the X11 forcing. - Pins
WAYLAND_DISPLAY(defaultwayland-0) andXDG_SESSION_TYPE=wayland— SDL3 reports "wayland not available" without them, and tmux servers started under an X11 login feed exactly that stale combo to anything launched through them. - Sets
__NV_DISABLE_EXPLICIT_SYNC=1— see the compositor-disconnect note below. - Sets
SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1so controller input still reaches UE when SDL's Wayland focus tracking is unset; controller input also remains active while the editor is unfocused.
Required engine patches: four, all driver-gated so X11 is untouched.
patches/ue-5.7-wayland-native-popups.md (Perforce CL 7105) makes menus/tooltips real
SDL3 popups; patches/ue-5.7-wayland-swapchain-teardown.md (CL 7129) destroys the
Vulkan swapchain before SDL destroys the wl_surface (fixes the compositor disconnect —
see below); patches/ue-5.7-wayland-popup-teardown-order.md (CL 7196) destroys anchored
popups before their anchor window so the CL 7129 drain also covers popups SDL would
otherwise cascade-destroy; patches/ue-5.7-wayland-modal-input-frame.md (CL 7197)
re-syncs Slate's window position to SDL's after compositor-rejected toplevel moves so
modal dialogs accept clicks. Each .md is a full per-file change reference, detailed enough to
re-implement on any engine drop. The actual diffs are not published here — engine source
is Epic-EULA-restricted — they live in Perforce and apply with patch -p1 from the UE
root if you have the .patch files locally (git-ignored).
Stock UE creates every Slate menu/tooltip as an unparented toplevel typed via X11-only
hints; Wayland compositors cannot be told where to place toplevels, so KWin centers
them all, and the resulting overlap of believed window rects misroutes hit tests
(dead submenus, unclickable modals, misplaced tooltips). The patch:
- Creates
EWindowType::Menu/ToolTipwindows as real SDL3 popups (xdg_popup) anchored to a parent with parent-relative coordinates (driver-gated; X11 untouched). - Converts popup coordinates global↔parent-relative at every UE/SDL boundary.
- Recreates the tooltip window per hover (Wayland popups cannot re-anchor to a new parent, and Slate otherwise reuses one tooltip window forever).
- Invalidates cached popup handles before their parent window is destroyed (SDL destroys child popups recursively — prevents a use-after-free).
Compositor disconnect — FIXED (engine patch patches/ue-5.7-wayland-swapchain-teardown.md,
Perforce CL 7129, 2026-07-16). The symptom was Wayland display connection closed by server (fatal), always right after a window closed during a heavy map load (e.g. the
Open Asset dialog). Root cause: FSlateRHIRenderer::OnWindowDestroyed drops the last
ref to the window's FVulkanViewport, but as an FRHIResource its destructor — which
runs vkDestroySwapchainKHR and severs the swapchain↔wl_surface tie in the NVIDIA WSI
— is deferred into the RHI pending-delete queue that FlushRenderingCommands never runs
(and neither does a plain GPU-idle flush). The swapchain outlived the window, so
SDL_DestroyWindow killed the wl_surface while the WSI still owned it, and a late
present attached to the dead surface → fatal wl_display error: invalid object N → KWin
disconnects UE, every window vanishes, process lingers headless spamming
AcquireNextImage() failed / Swapchain surface lost (no crash report, no Xid — not
a GPU fault; kill-unreal it). The patch, Wayland-gated in OnWindowDestroyed, idles
the GPU then forces the RHI deferred-delete queue
(ImmediateFlush(FlushRHIThreadFlushResources)) so vkDestroySwapchainKHR runs while
the surface is still valid. __NV_DISABLE_EXPLICIT_SYNC=1 (below) is kept as
belt-and-suspenders — it removed an earlier manifestation of the same race
(wp_linux_drm_syncobj_surface_v1: Surface got destroyed already); the patch fixes the
race itself. If a disconnect ever recurs, capture it:
WAYLAND_DEBUG=1 run-unreal ... 2>trace.log and read the last requests naming
the reported object id before the error line.
Other Wayland notes: modal dialogs are compositor-placed (centered on parent — fine).
Modals that Slate repositions after mapping (autosized startup dialogs like "source
content changes detected") used to ignore all client-area clicks — Slate's speculative
position cache diverged from the input frame SDL builds emulated cursor coords from —
fixed by patches/ue-5.7-wayland-modal-input-frame.md (CL 7197, verified
2026-07-21). If a modal ever sticks again: Alt+Tab → Esc/Enter or the server-side
decoration ✕.
The X11 exit() crash class (see below) cannot occur — libX11 is not in the stack.
The p4 CLI is included in the FHS environment, so it's available inside unreal-fhs without any extra setup.
For Rider's Perforce integration to work, you also need p4 installed at the NixOS system level, since Rider runs outside the FHS sandbox. Add it to your configuration.nix:
environment.systemPackages = with pkgs; [
p4
];After every Perforce sync, you'll need to fix execute permissions since P4 doesn't preserve Unix file modes:
chmod +x Setup.sh GenerateProjectFiles.sh Engine/Binaries/DotNET/GitDependencies/linux-x64/GitDependencies
find Engine/Build -name "*.sh" -exec chmod +x {} +Rider's default 2GB max heap is not enough for indexing UE5 engine source — it will hang on "Loading projects." Create a custom vmoptions file:
File: ~/.config/JetBrains/Rider2025.1/rider64.vmoptions
Copy the defaults from <rider-install>/bin/rider64.vmoptions and change:
-Xms2048m # initial heap (default: 128m)
-Xmx8192m # max heap (default: 2048m)
Adjust the version in the path (Rider2025.1) to match your Rider version.
When using JetBrains Rider on Linux, it may auto-detect the .git directory and default the VCS integration to Git. If your project uses Perforce:
- Go to Settings > Version Control > Directory Mappings
- Change the VCS for your project root from Git to Perforce
- Go to Settings > Version Control > Perforce and set:
- Server (Port):
ssl:genun.helixcore.io:1666 - User: your Perforce username
- Workspace (Client): your Linux workspace name
- Path to P4 executable:
/etc/profiles/per-user/<your-user>/bin/p4
- Server (Port):
- Click Test Connection to verify
C++ hot reload (patching a running editor without restarting it) works on Linux, but three
environment-specific problems had to be solved. The ascent-hot-reload script (home-manager
modules/home/dev/unreal.nix in the nixos repo; Space r h in nvim) wraps the working invocation:
Build.sh AscentRivalsEditor Linux Development -project=<uproject> -WaitMutex -ForceHotReload -NoUBA
- UBT cannot detect the running editor. The editor records its PID in
Engine/Intermediate/EditorRuns/, but that PID belongs to theunreal-fhsnamespace, so on the host UBT sees a dead PID, deletes the file, and silently falls back to a normal build (unsuffixed.so, editor never reloads). Fix: always pass-ForceHotReloadwhile the editor runs — the script gates onpgrep -x UnrealEditor. - UBA overhead. The Unreal Build Accelerator session server adds seconds of startup/teardown
to a 3-action build.
-NoUBAuses the plain parallel executor. - Editor crash during reload via PythonScriptPlugin (a hard dependency of
McpAutomationBridge): regenerating python wrappers for the reloaded module corrupts the heap
(abort/segfault in
FPyWrapperTypeRegistry::RegisterPythonFieldName, reproducible under both-ansimallocand default mimalloc). Fixed by a guard inFPythonScriptPlugin::OnModulesChangedthat skips wrapper regeneration whileIsReloadActive()— submitted as p4 change 7484. Python wrappers for a hot-reloaded module stay stale until the next editor restart.
Hot reload remains body-of-function level: layout changes (UPROPERTY/UFUNCTION, headers) still need a full rebuild and editor restart.
UBT's adaptive unity build excludes writable (checked-out) files from unity chunks so a single-file edit compiles alone (~12s total instead of ~20s for a full chunk). Two requirements:
- The file must actually be
p4 edit-ed. In nvim,:pwruns edit-then-write; files saved with:w!stay read-only on disk and rebuild their entire unity chunk every time. McpAutomationBridge.Build.csused reflection to disable adaptive unity target-wide (a workaround for installed launcher engines where all plugin sources are writable). Scoped toTarget.bIsEngineInstalledin p4 change 7484.
Checking out a widely-depended-on engine file (e.g. PythonScriptPlugin.cpp) drags its whole
dependent chain into the hot-reload relink set (~46 extra small links per reload). A full
ascent-rebuild with the editor closed resets the suffix state and shrinks reloads back to
the changed file plus two links.
run-unreal automatically applies these flags: -ansimalloc -reducethreadusage
These prevent mimalloc-related crashes and reduce memory pressure during debugging.
(-limitedmemorypool was previously recommended here but has been removed: it caps UE's
allocator pool at ~11GB, which starves async asset compilation — first-time loads of large
meshes need 15-30GB. The cgroup limit below provides the OOM protection instead.) However, IDEs launch their own UE process, so you must add the flags there too.
- Run > Edit Configurations...
- Select your Uproject configuration
- In the Optional arguments field, add:
-ansimalloc -reducethreadusage - Click Apply
Debug flags are configured in .zed/debug.json (launch config args field). The existing launch config already includes the project path — add the stability flags there:
"args": ["<path/to/YourProject.uproject>", "-ansimalloc", "-reducethreadusage"]Any IDE or tool that launches UnrealEditor directly will need these same flags passed as program arguments. Consult your IDE's run/debug configuration for where to set them.
A cgroup memory limit is active in scripts/unreal/run.nix (96G, sized for 128GB RAM — leaves ~32GB for the OS and other GPU/inference workloads). UE gets killed cleanly instead of freezing the system when it runs away. Adjust to your RAM.
Zed uses clangd for C++ intellisense, which needs a compile_commands.json generated from UnrealBuildTool.
cd UE/flake && nix develop
unreal-fhs
gen-compile-commands # generates UE/compile_commands.json (takes a few minutes)Then launch with:
run-zed # launches Zed inside FHS so clangd resolves all pathsProject-level settings live in .zed/settings.json (already configured to point clangd at UE/compile_commands.json). Debug configs are in .zed/debug.json with CodeLLDB attach/launch targets and LLDB symbol preloading enabled.
Re-run gen-compile-commands after adding new source files or modules so clangd picks them up.
| Zed | Rider | |
|---|---|---|
| Strengths | Fast startup, low RAM (~500MB vs Rider's 4-8GB), responsive on large codebases, native Linux feel, keyboard-driven workflow | Full UE integration (Blueprints, asset browser, UBT build), mature debugger UI, Perforce UI, refactoring tools |
| Best for | C++ editing & navigation, quick code changes, lightweight debugging sessions, working alongside UE editor | Full project work (code + assets + Blueprints), heavy debugging with watch/conditional breakpoints, Perforce operations |
| Weaknesses | No Blueprint support, no UE asset integration, clangd can be slow on initial index, less polished debugger UI | Heavy RAM usage (4-8GB+), slow startup, can feel sluggish with large engine codebase |
| Debugging | CodeLLDB (DAP) — functional but minimal UI | Full LLDB integration with inline values, conditional breakpoints, memory views |
| Intellisense | clangd (needs gen-compile-commands first, re-run when adding files) |
Built-in via ReSharper C++ (works automatically with .uproject) |
Recommendation: Use Zed for day-to-day C++ editing when you mostly need fast navigation and quick edits. Switch to Rider for debugging sessions that need conditional breakpoints, Blueprint work, or Perforce operations.
A PostToolUse hook (.claude/hooks/open-in-zed.sh) automatically opens files in Zed as Claude Code edits them. Configured in .claude/settings.local.json (not committed — local machine preference).
Workflow:
run-zedto launch Zed- Split the editor right (
Ctrl+K, Ctrl+\), click in the right pane Alt+Cto spawn Claude Code in the bottom terminal panel- As Claude edits files, they open in the last-focused editor pane
Limitation: Zed's CLI (zeditor -a) has no --pane flag to target a specific split pane. Files open in whichever pane was last focused. Click the right pane to redirect. Zed is open source (MIT, Rust) — adding a --pane flag is a future contribution opportunity. Relevant code: crates/cli/src/main.rs, crates/workspace/src/pane.rs, crates/zed/src/open_listener.rs.
Zed tasks (.zed/tasks.json): Alt+C runs Claude Code, "Enter FHS" available from command palette.
Two config-level fixes for recurring editor crashes that look like GPU faults but aren't.
Both are console variables set in an [ConsoleVariables] section of an Engine ini read at
startup — project-level <Project>/Config/<Platform>/<Platform>Engine.ini is the durable
place (but see the Fab gotcha below).
UE embeds Chromium (CEF, running as EpicWebHelper) for the marketplace/docs panels. Under
the FHS sandbox its GPU subprocess is unstable: it crashes (SIGILL in libcef.so) or resets
the GPU it shares with the editor, killing UE with VK_ERROR_DEVICE_LOST — an empty device
fault report and no kernel Xid. The embedded browser doesn't need GPU compositing, so
disable it:
[ConsoleVariables]
r.CEFGPUAcceleration=0Gotcha — plugin config outranks project config: Epic's Fab plugin ships
Engine/Plugins/Fab/Config/Engine.ini containing r.CEFGPUAcceleration=true, which
silently overrides the project-level setting. Either edit that plugin ini to false, or
put the cvar in <Project>/Saved/Config/<Platform>Editor/Engine.ini (the only layer above
plugins — but Saved configs get wiped/regenerated, so it regresses silently).
Verify on launch: the log must say LogCEFBrowser: CEF GPU acceleration disabled.
Alternative if you never use the embedded browser: pass -nocef.
Hovering pins in Blueprint graphs summons the pin value inspector popup
(SGraphPin::Tick -> FPinValueInspectorTooltip::SummonTooltip) — a real native window
with its own Vulkan swapchain, created and destroyed on every hover. In the Anim
Blueprint editor it is always armed (the preview instance counts as a debug object), so
just mousing across a graph churns window+swapchain create/destroy nonstop. On
NVIDIA/Linux this kills the editor two different ways, which is why it's so confusing:
VK_ERROR_DEVICE_LOSTwith an empty fault report and no kernel Xid — the swapchain churn takes the device down (a grey, never-rendered popup on screen at death is the tell), or- instant silent exit — dismissal (
SDL_HideWindow -> XTranslateCoordinates) hits an X11 protocol error and libX11's default handler callsexit(). No crash report, no coredump, log just stops.
The fix (confirmed to stop both): Editor Preferences → Blueprint Editor Settings →
uncheck Enable Pin Value Inspection Tooltips (bEnablePinValueInspectionTooltips).
Two traps:
Slate.EnableTooltips=0does not help — this popup is not a Slate tooltip and bypasses that cvar entirely. Regular tooltips are safe to leave on.- The setting persists in
Saved/Config/.../EditorPerProjectUserSettings.ini, which UE wipes/regenerates — if Blueprint-graph crashes come back "out of nowhere", re-check the checkbox first.
For the exit() failure mode there's also a proper engine-source fix: install non-fatal
X error handlers (XSetErrorHandler/XSetIOErrorHandler, dlopen-based so no link dep)
after SDL_Init in FLinuxPlatformApplicationMisc::InitSDL() — protocol errors become
log warnings instead of process death. Worth it if you build from source; it can't prevent
the device-loss mode, though — only the setting above removes the trigger.
r.CEFGPUAcceleration=0 does not stop Chromium from spawning a GPU process — under
the FHS sandbox that process fails ANGLE/Vulkan init (VK_ERROR_INITIALIZATION_FAILED),
exits, and Chromium respawns it every ~100ms, forever, spamming the terminal and
poking the Vulkan driver from inside your editor session. -nocef is the only complete
off switch if you don't need the embedded browser.
On any editor "GPU crash": check journalctl -b 0 | grep -iE 'NVRM|Xid' (beware: grep -i xid also matches r8169 ethernet lines). No NVIDIA Xid + empty fault report = the GPU
never faulted — suspect the two classes above, or an external GPU-client reset: any
Chromium/Electron app sharing the card (Firefox WebRender, Discord, Spotify, Steam webviews,
JetBrains' JCEF) can force a device reset that takes UE down. Disable hardware acceleration
in those apps, or move them to another GPU.
- Wayland: Popup keyboard input may not work. Run
kde-wayland-settingsfor workaround info. - X11 fallback:
run-unreal --x11setsGDK_BACKEND=x11,QT_QPA_PLATFORM=xcb,SDL_VIDEODRIVER=x11,SDL_VIDEO_DRIVER=x11. - Perforce permissions: P4 doesn't preserve Unix execute bits. See chmod notes in build steps above.
Originally forked from Adrastie/NixOSUnreal.