Lightmass not Working In Unreal 5.7 on Mac

[UE 5.7 / Apple Silicon] Lightmass dies 1.2s after launch — Embree returns RTC_ERROR_UNSUPPORTED_CPU under Rosetta. Fix: bUseEmbree=false

TL;DR — On Apple Silicon, UnrealLightmass ships as an x86_64-only binary, so it runs under Rosetta 2. Embree cannot initialise there. Lightmass asserts and dies about a second after launch, and all you see in the editor is Failed to build lighting!!! followed by an unrelated-looking crash. The fix is one project config file:

; Config/Mac/MacLightmass.ini
[DevOptions.StaticLighting]
bUseEmbree=false
bVerifyEmbree=false
bUseEmbreePacketTracing=false
bUseEmbreeInstancing=false

That drops Lightmass back to its kDOP ray tracer. Slower, but it completes. My 5680-mapping level baked in 49 minutes on an M4.


Environment

Engine UE 5.7.4-51494982, Epic Launcher installed build
Machine MacBook Air M4, 10 cores, 16 GB
OS macOS 26.5.1 (25F80)
Level ~5700 lightmap mappings, static lighting only, no Lumen

What you actually see

In the editor log, a lighting build that dies in a handful of seconds:

LogStaticLightingSystem: Running Lightmass w/ ImmediateImport mode ENABLED
LogEditorBuildUtils: Build time 0:00
LogStaticLightingSystem: Warning: Failed to build lighting!!! Lighting build failed.

Then, usually, the editor takes an assert on the way out:

Assertion failed: !bUpdating [File:./Runtime/Renderer/Private/VT/VirtualTextureSystem.cpp] [Line: 860]

FVirtualTextureSystem::RemoveAllProducerDestroyedCallbacks
FMaterialRenderProxy::~FMaterialRenderProxy
FLightmassMaterialProxy::~FLightmassMaterialProxy
FMaterialExportDataEntry::~FMaterialExportDataEntry
FLightmassExporter::~FLightmassExporter
FLightmassProcessor::~FLightmassProcessor
FStaticLightingSystem::~FStaticLightingSystem
FStaticLightingManager::FailLightingBuild(FText)
FStaticLightingSystem::UpdateLightingBuild()

That assert is not your bug. It is an engine bug on the failure path: FailLightingBuild destroys the exporter, whose material proxies unregister from the virtual texture system at a moment the VT system does not expect. It fires because the build already failed. Chasing it is a dead end.

UnrealLightmass’s own log, in ~/Library/Logs/Unreal Engine/UnrealLightmass/, stops dead at:

Building static lighting...

and tells you nothing else. That truncation is itself a symptom — see below.


Things that are NOT the cause

I burned a lot of time on these. Save yourself the trouble:

  • Missing or quarantined binary. Engine/Binaries/Mac/UnrealLightmass exists, is executable, has no Gatekeeper quarantine xattr.
  • The binary not starting. It starts fine. It even prints its full banner and reaches Building static lighting....
  • Swarm directories missing. They are not missing, they are somewhere most people do not look. FSwarmInterfaceLocalImpl::OpenJob builds the path from FPaths::GameAgnosticSavedDir(), which on Mac is ~/Library/Application Support/Epic/UnrealEngine/<ver>/Saved/Swarm/SwarmCache/Jobs/. Check there before concluding the export failed. Mine had the full 26 MB .scenegz plus 5749 task files. The export side works.
  • Memory. Peak RSS of the dying Lightmass process was 122 MB. Not an out-of-memory kill.
  • Code signing. UnrealLightmass is not signed at all (codesign -v says “code object is not signed at all”, spctl rejects it, macOS logs it in TCC as identifier=<ID of InvalidCode>). Unsigned x86_64 still executes, so this is not fatal — but it does explain why firewall allow-rules for it do not survive reinstalls.

The one that will genuinely mislead you: the 60-second multicast timeout

There is a separate, real failure with the same editor-facing symptom. On Mac, the editor and Lightmass are two processes that talk over the UDP message bus rather than internal pipes, and they discover each other via multicast:

LogUdpMessaging: Initializing bridge on interface 0.0.0.0:0 to multicast group 230.0.0.1:6666
LogInit: Error: Timed out waiting for the recipient (TimeWaitingSec = 60.085411)

FSwarmInterfaceLocalImpl::SendMessage waits up to 60 seconds for the peer. If the firewall or the Local Network privacy permission blocks it, both sides sit there and the build fails after roughly a minute.

Tell the two apart by how long the failure takes:

Failure time Cause
~60–100 s, with Timed out waiting for the recipient messaging / firewall / Local Network permission
~1–8 s, no timeout line the Embree problem in this post

Fixing the firewall problem moved me from the first row to the second, which is exactly why it looked like “the fix made no difference”.


Finding the real error

The editor launches Lightmass with -nostdout, and Lightmass’s log file loses its final lines because the process exits via exit() and then crashes inside static destructors (FOutputDeviceRedirector::~FOutputDeviceRedirectorFThread::~FThread), taking the unflushed tail with it. So the one message you need is the one you never get.

To capture it, temporarily wrap the binary:

B="/Users/Shared/Epic Games/UE_5.7/Engine/Binaries/Mac"
mv "$B/UnrealLightmass" "$B/UnrealLightmass.real"
cat > "$B/UnrealLightmass" <<'SH'
#!/bin/sh
REAL="/Users/Shared/Epic Games/UE_5.7/Engine/Binaries/Mac/UnrealLightmass.real"
LOG="/tmp/lightmass_capture.txt"
ARGS=""
for a in "$@"; do [ "$a" = "-nostdout" ] && continue; ARGS="$ARGS $a"; done
/usr/bin/time -l "$REAL" $ARGS >> "$LOG" 2>&1
SH
chmod +x "$B/UnrealLightmass"

:warning: Important: do not leave that wrapper installed

It will break the bake even after you fix Embree, in a way that looks like a content problem:

Assertion failed: CurrentMaterialElement.Material
[File:./Programs/UnrealLightmass/Private/Lighting/LightingMesh.cpp] [Line: 146]
Failed to import material with Hash 6A92569E5C0C48E0F9255BCBD91B4EF91A6C1400

Material channels are named by CreateChannelNameWithLMExecutableHash (ImportExport.h):

v<version>.<material hash>.<SHA1 OF THE LIGHTMASS EXECUTABLE>.mtrlgz

That second hash exists so a stale cache from a different build cannot be picked up. With the wrapper in place the editor hashes the shell script, while Lightmass — now running from UnrealLightmass.real — hashes itself. I verified both:

File SHA1 Where it appears
wrapper script E42E5BD7C70EB17E0043B8F206867F79630620D1 the filenames actually written to the job folder
real binary C8BCC6620B9A2794F030665122F275DC0FD4E72B the filename Lightmass asks for

The .mtrlgz is on disk under the wrong name, OpenChannel fails, the material comes back null, assert. The scene channel uses CreateChannelName, which carries no executable hash, which is why the scene itself loads fine and only materials break.

There is no way to fix this in the wrapper — the editor and the child would have to hash the same file. Use it to see why Lightmass dies at startup, then remove it.


The real error

Assertion failed: rtcGetDeviceError(EmbreeDevice) == RTC_ERROR_NONE
[File:./Programs/UnrealLightmass/Private/ImportExport/LightmassScene.cpp] [Line: 119]

UnrealLightmass-Core.dylib!FDebug::CheckVerifyFailedImpl2
UnrealLightmass!Lightmass::FScene::Import
UnrealLightmass!Lightmass::FLightmassImporter::ImportScene
UnrealLightmass!Lightmass::BuildStaticLighting
UnrealLightmass!Lightmass::LightmassMain

Lightmass dies inside FScene::Import, creating the Embree device. Exit code 3, about 3 seconds wall clock, 122 MB peak RSS.


Proof that it is Rosetta, not Embree

UnrealLightmass is x86_64 only:

$ lipo -archs Engine/Binaries/Mac/UnrealLightmass
x86_64
$ lipo -archs Engine/Binaries/Mac/UnrealEditor.app/Contents/MacOS/UnrealEditor
x86_64 arm64

The editor is universal and runs native arm64; Lightmass is translated. The Embree dylib shipped with the engine, however, is universal:

$ lipo -archs Engine/Binaries/Mac/libembree4.4.3.3.dylib
x86_64 arm64

So I called rtcNewDevice on the exact same dylib from both slices:

// clang -arch x86_64 -o emb emb.c     (and again with -arch arm64)
#include <stdio.h>
#include <dlfcn.h>
typedef void* RTCDevice;
int main(void){
  void* h = dlopen("/Users/Shared/Epic Games/UE_5.7/Engine/Binaries/Mac/libembree4.4.dylib", RTLD_NOW);
  RTCDevice (*newdev)(const char*) = dlsym(h, "rtcNewDevice");
  int (*geterr)(RTCDevice) = dlsym(h, "rtcGetDeviceError");
  const char* cfgs[] = {NULL, "isa=sse2", "isa=sse4.2", "isa=avx", "isa=avx2", "isa=avx512"};
  for (int i = 0; i < 6; i++) {
    RTCDevice d = newdev(cfgs[i]);
    printf("cfg %-12s device=%p error=%d\n", cfgs[i] ? cfgs[i] : "(default)", d, geterr(d));
  }
  return 0;
}

Results, Embree 4.3.3:

Slice ISA configs tried Result
x86_64 under Rosetta 2 default, sse2, sse4.2, avx, avx2, avx512 RTC_ERROR_UNSUPPORTED_CPU (5) on all six
arm64 native default, sse2, sse4.2 RTC_ERROR_NONE (0), device created

Same file, same machine, same moment. The arm64 half works. No isa= string and no ~/.embree4 config file helps, because the problem is that Embree’s CPU feature detection does not find a CPU it will accept when running translated.


The fix

Engine/Config/BaseLightmass.ini has:

[DevOptions.StaticLighting]
bUseEmbree=true

The editor reads that key and writes it into the exported scene (Lightmass.cpp, Scene.GeneralSettings.bUseEmbree), so a project-level override reaches the Lightmass process. With Embree off, Lightmass falls back to its own kDOP tree, which is pure C++ and perfectly happy under Rosetta.

Put it in a platform-scoped file so Windows bakes keep Embree and their speed:

YourProject/Config/Mac/MacLightmass.ini
[DevOptions.StaticLighting]
bUseEmbree=false
bVerifyEmbree=false
bUseEmbreePacketTracing=false
bUseEmbreeInstancing=false

Restart the editor so the ini hierarchy is re-read, then build lighting.

You can confirm the fallback is active in the Lightmass log:

FStaticLightingSystem started using GKDOPMaxTrisPerLeaf: 4

Result

Lightmass on <host>: 49:23 min total, 753.159 ms importing, 4.28 sec setup,
5:05 min photons, 44:11 min processing, 18.900 ms extra exporting
[5680/5680 mappings].
Threads: 8:06:08 hours total, 5:15:38 hours processing.

All 5680 mappings completed, Volumetric Lightmap imported, _BuiltData written at 312 MB. Importantly for my case: the resulting lightmaps render correctly under Metal, whereas a _BuiltData copied byte for byte from a Windows bake had whole panels rendering black on the same Mac.


Caveats

  • It is slower. Embree exists for a reason. kDOP is the old path. Budget accordingly on big levels.
  • Measured CPU frequency: 1.00 GHz appears in the Lightmass log. Rosetta does not expose the real clock, so Lightmass’s internal cost estimates and predicted times are meaningless. The bake itself is fine.
  • Memory. Two UE processes on a 16 GB machine is tight. Closing a browser took my swap usage from 1.13 GB to 592 MB during the bake.
  • Lighting quality level is not recorded in any ini or log — it lives in Build > Lighting Quality. Check it before you assume a 49-minute bake is your Production number.

The proper fix, for Epic

Ship an arm64 UnrealLightmass for Apple Silicon. The Embree dylib in the engine is already universal; only the executable is not, and that single fact is what forces the whole thing through Rosetta. The installed build does not include Lightmass’s Private/ sources, so you cannot rebuild it yourself without cloning full engine source.

Until then, bUseEmbree=false is a one-line, platform-scoped workaround that does not touch code, the engine install, or your Windows pipeline.