Skip to content
 
 

Repository files navigation

gUbGuuB NixOS Unreal Dev Env


NixOS FHS flake/develop env for Unreal Engine.


Usage:

Build Unreal Engine from sources:

  1. You must have access to the Unreal Engine source (via Perforce depot or Epic GitHub)
  2. Pull from your Perforce depot, or git clone git@github.com:EpicGames/UnrealEngine.git --branch release --single-branch
  3. Inside the UE/ folder, run git clone git@github.com:Yagi-Michael/NixOSUnreal.git flake
  4. cd flake && nix develop
  5. unreal-fhs
  6. cd ..
    • 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
  7. ./Setup.sh
  8. ./GenerateProjectFiles.sh
    • Note: You may need to fix permissions on engine build scripts first: find Engine/Build -name "*.sh" -exec chmod +x {} +
  9. make -j1 we must use -j1
  10. Once compiled you will find the binary at Engine/Binaries/Linux/UnrealEditor

Example structure:

UE
├── <YourProject>      (game project)
├── Engine             (engine source)
├── flake <---|        Put flake here
├── Setup.sh
└── GenerateProjectFiles.sh

Daily Workflow

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)

Available Commands

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)

Packaged Test runner (run-ascent-test)

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.

Native Wayland (default)

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 env

What 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 (default wayland-0) and XDG_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=1 so 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:

  1. Creates EWindowType::Menu/ToolTip windows as real SDL3 popups (xdg_popup) anchored to a parent with parent-relative coordinates (driver-gated; X11 untouched).
  2. Converts popup coordinates global↔parent-relative at every UE/SDL boundary.
  3. Recreates the tooltip window per hover (Wayland popups cannot re-anchor to a new parent, and Slate otherwise reuses one tooltip window forever).
  4. 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.

Perforce on NixOS

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 IDE Setup

Increase Heap Size (Required for UE5 Source)

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.

Perforce Integration

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:

  1. Go to Settings > Version Control > Directory Mappings
  2. Change the VCS for your project root from Git to Perforce
  3. 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
  4. Click Test Connection to verify

Hot Reloading

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
  1. UBT cannot detect the running editor. The editor records its PID in Engine/Intermediate/EditorRuns/, but that PID belongs to the unreal-fhs namespace, 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 -ForceHotReload while the editor runs — the script gates on pgrep -x UnrealEditor.
  2. UBA overhead. The Unreal Build Accelerator session server adds seconds of startup/teardown to a 3-action build. -NoUBA uses the plain parallel executor.
  3. 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 -ansimalloc and default mimalloc). Fixed by a guard in FPythonScriptPlugin::OnModulesChanged that skips wrapper regeneration while IsReloadActive() — 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.

Iteration speed

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, :pw runs edit-then-write; files saved with :w! stay read-only on disk and rebuild their entire unity chunk every time.
  • McpAutomationBridge.Build.cs used reflection to disable adaptive unity target-wide (a workaround for installed launcher engines where all plugin sources are writable). Scoped to Target.bIsEngineInstalled in 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.

Debugging Stability

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.

Rider

  1. Run > Edit Configurations...
  2. Select your Uproject configuration
  3. In the Optional arguments field, add: -ansimalloc -reducethreadusage
  4. Click Apply

Zed

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"]

Other IDEs

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.

Memory Limits

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 IDE Setup (WIP)

Zed uses clangd for C++ intellisense, which needs a compile_commands.json generated from UnrealBuildTool.

First-Time Setup

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 paths

Project-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 vs Rider — When to Use Which

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.

Claude Code Integration

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:

  1. run-zed to launch Zed
  2. Split the editor right (Ctrl+K, Ctrl+\), click in the right pane
  3. Alt+C to spawn Claude Code in the bottom terminal panel
  4. 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.

Editor Crash Mitigations (Linux)

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).

CEF (embedded browser) GPU process kills the editor

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=0

Gotcha — 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.

Blueprint pin-hover popups crash the editor (the "tooltip crash" that isn't a tooltip)

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:

  1. VK_ERROR_DEVICE_LOST with 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
  2. instant silent exit — dismissal (SDL_HideWindow -> XTranslateCoordinates) hits an X11 protocol error and libX11's default handler calls exit(). 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=0 does 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.

CEF GPU process crash-loop (even when "disabled")

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.

Triage rule of thumb

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.

Known Issues

  • Wayland: Popup keyboard input may not work. Run kde-wayland-settings for workaround info.
  • X11 fallback: run-unreal --x11 sets GDK_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.

About

My NixOS Flake to run UnrealEngine Linux Binary & JetBrain Rider

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages