Open a menu once in one specific map, on macOS only, and every click stops landing — not on that map, on any map, for the rest of the process. The menu still opens. It still draws. Nothing you click does anything. My existing instrumentation for this project answers exactly one question, whether a menu opened, and a menu can be open, drawn, and completely unclickable at the same time. That question told me nothing.
This post is about building the instrument that could actually see the fault, what it found once it could — a window reporting a screen position two screen-heights tall that it never occupied — and the part that mattered most once I had the engine-level explanation in hand: the thing that triggered it, on this one map, was a decision in my own scene, not the engine.
What eos.ProbeMenu couldn’t tell me
The project already had a console command that answers “did the menu open.” It’s useless here because the menu genuinely does open. What’s missing is everything about who owns the mouse once it has: whether the cursor is shown, whether the viewport is capturing it, whether it’s actually locked, whether move and look input are being suppressed the way a menu’s input config is supposed to suppress them. FInputStateProbe reports five of those facts at named points in a run — arrival, menu activated, the frame after, menu closed — so a session reads as a sequence instead of one snapshot:
UE_LOG(LogGame, Display,
TEXT("[Input] %s map=%s show-cursor=%s capture=%s lock=%s has-capture=%s ignore-move=%s ignore-look=%s"),
Occasion, ..., CaptureModeName(Viewport->GetMouseCaptureMode()),
LockModeName(Viewport->GetMouseLockMode()), YesNo(bHasCapture),
YesNo(Controller->IsMoveInputIgnored()), YesNo(Controller->IsLookInputIgnored()));
It also reports the window’s own screen position, next to those five, because an earlier decision on this project had already flagged a window reporting the wrong place as a second candidate for a dead mouse on macOS — and nothing above would show it.
That second half of the probe found the fault on the first hand-played session. Every one of the five input facts was identical between a working map and the broken one: same capture mode, same lock mode, same suppression flags. CommonUI’s input config was doing exactly what it was supposed to do. The mouse was locked out somewhere the config couldn’t see.
The window that stopped telling the truth
The window position sat at (0, 0) on arrival, correct. Two frames later it read (0, 2879), on a 2560×1440 display, and it never moved again for the rest of the process — 257 log lines at (0, 0), then 983 at (0, 2879), zero recoveries. 2879 is 2 × 1440 − 1, a value that doesn’t depend on the window’s own height; a run at 720 tall reported the same number.
The mouse itself worked fine. Three clicks taken before the fault landed at screen Y 533, 577 and 818 — comfortably inside a 1440-tall client rect. Forty-five clicks taken once the fault was live, on a packaged build, all landed at an in-window Y between −1743 and −2870. Every one of them was outside the window by roughly two screen heights, and clicking harder didn’t help: mashing the mouse brings the OS cursor back on screen, but the coordinate behind it stays at 2879, so it looks alive and does nothing — which is exactly the symptom as reported.
The reason is one line in the engine, SWindow::IsScreenspaceMouseWithin (Runtime/SlateCore/Private/Widgets/SWindow.cpp:1615-1619, UE 5.8.1):
bool SWindow::IsScreenspaceMouseWithin(UE::Slate::FDeprecateVector2DParameter ScreenspaceMouseCoordinate) const
{
const FVector2f LocalMouseCoordinate = ScreenspaceMouseCoordinate - ScreenPosition;
return !LocalMouseCoordinate.ContainsNaN() && NativeWindow->IsPointInWindow(...);
}
ScreenPosition is a member of SWindow, not a live query — and FSlateApplication::LocateWindowUnderMouse gates on this function, so a click’s local coordinate comes out wrong before any widget ever sees it. The figure below is that subtraction at the actual measured scale: the window’s real client rect on one line, and the range every one of the 45 clicks landed in on the other.
Correcting that one cached value would be enough on its own — FMacWindow::IsPointInWindow (Runtime/ApplicationCore/Private/Mac/MacWindow.cpp) never consults screen position at all; it builds its rect at (0, 0) from the window’s own frame. It does carry a second, independent gate, WindowHandle->bIsOnActiveSpace, a cached flag nothing in this project’s instrumentation reads yet — so whether that gate was also wrong while the fault was live is still open.
Where the bad value gets written, and why it never comes back
FMacApplication::OnWindowDidMove (Runtime/ApplicationCore/Private/Mac/MacApplication.cpp:1393-1429) branches on the window’s mode label:
if ([Window->GetWindowHandle() windowMode] == EWindowMode::Fullscreen)
{
// Fullscreen mode always moves to 0,0
MessageHandler->OnMovedWindow(Window, 0, 0);
}
else
{
const double X = WindowFrame.origin.x;
const double Y = WindowFrame.origin.y + OpenGLFrame.size.height;
...
FVector2D SlatePosition = ConvertCocoaPositionToSlate(X, Y);
MessageHandler->OnMovedWindow(Window, TruncToInt(SlatePosition.X), TruncToInt(SlatePosition.Y));
}
Only exclusive EWindowMode::Fullscreen is protected by the constant (0, 0). Everything else — including WindowedFullscreen, which is what this project measured itself running in, from both SWindow::GetWindowMode() and UGameUserSettings::GetFullscreenMode() — is computed from the Cocoa window’s frame and openGLFrame at the instant the OS notification is processed. If that instant lands mid-transition, the computed value can be anything, and there’s a comment sitting three lines above it that says Epic has already fixed one version of exactly this race:
// Update ScreensArray when displays are reconfiguring to have up to date informations
// This is to prevent UE-176002 that happened because UpdateScreensArray is called once
// the display reconfiguring is done but OnWindowDidResize is call during the reconfiguring
if (bDisplayReconfiguring) { UpdateScreensArray(); }
That guard covers UpdateScreensArray. It says nothing about the position computed two lines later. What’s downstream is SWindow::SetCachedScreenPosition, which just assigns ScreenPosition — there’s no polling, no reconciliation, nothing that re-checks the value against anything. It stands until the next NSWindowDidMoveNotification, NSWindowDidResizeNotification, or fullscreen enter/exit notification for that window. A game that never moves its window again gets that value permanently.
Which of those four notifications actually fired isn’t established — they’re engine-internal and unlogged, and answering it would need binding SWindow::SetOnWindowMoved and dumping a stack trace from inside the handler, which hasn’t been built yet.
An automated harness could not produce this
None of the above came from an unattended run. Every scripted repro this project has opens and closes a menu in the same instant, and that never produces the reported situation — the fault needs the menu to actually sit open over the broken input state for a person to notice, and “open, then immediately close” isn’t the same shape as “open and stay open while nothing responds.”
PostDriving Unreal from the command lineA multiplayer host-and-join flow that needs two people at two keyboards to test let a bug through twice, in two different shapes, before I built a command-line pipeline that runs it unattended — generating, editing, packaging, and driving Unreal without opening the editor.That’s a strange thing to run into after building a whole pipeline to drive this same game unattended — packaging, signing in, hosting, joining, all from the command line with nobody at a keyboard. This one fault sits exactly on the other side of that line. What got built for it is scripts/play-probed.sh: it arms the probes, hands the packaged build to a person, and keeps the log. The automated attempt, scripts/repro-room-menu.sh, is kept anyway — its verdict (three runs, not reproduced) is a real comparison and it’ll matter if the fault ever becomes reproducible without a person. play-probed.sh doesn’t print a verdict line either, and that’s deliberate: what counts as a verdict here is still a person saying what they saw.
The trigger was mine
Here’s the part I got wrong before I had all of this measured. It would have been easy to write this up as “hit a known Unreal bug on macOS, moved on” — and for a while that’s what I believed I was doing, because an earlier note on this project had already put a name to it and I hadn’t gone back to check that name against anything. The map where this happens is the only one that places the player twice.
Its spawn points arrive inside Level Instances, so they don’t exist yet the first time the engine looks for one. AGameModeBase::PostSeamlessTravel — which spawns the player, under the engine’s own comment “This may spawn the player pawn if the game is in progress” — runs before UWorld::BeginPlay does, which is also before anything in this project’s own load-wait logic gets a turn. So the engine tries first, finds nothing, and this project’s code repairs it a moment later with a second placement once the spawns actually exist. Every session where the window fault showed up had that second placement sitting right before it. The other maps in this project place a player once, on arrival, and never move the window.
The fix wasn’t aimed at the window at all. It stops the engine from placing anyone until the map is actually ready — bStartPlayersAsSpectators holds every controller, a wait now asks each Level Instance whether it’s visible, not merely loaded (ULevelInstanceSubsystem::IsLoaded only tests set membership; a level’s actors reach the world one state later, through MakingVisible to LoadedVisible — waiting on the wrong one once reported 66 of 128 spawn candidates present), and everyone gets placed exactly once, when it’s true. Two players, two round trips into the map afterward: no second placement, no window move beyond the one at startup, no (0, 2879), every click inside the window where it was clicked. The change was justified on its own terms first — a map whose contents arrive late is the normal case going forward, not this one map’s problem — and the window fault going quiet came along with it.
flowchart LR
A["every click stops<br/>landing, macOS only"] -->|"a window watcher,<br/>not the menu probe"| B["(0, 2879)<br/>never recovers"]
B -->|"45 clicks<br/>land outside"| C["SWindow caches it,<br/>FMacApplication writes it"]
C -->|"only where the player<br/>is placed twice"| D["place once:<br/>symptom gone,<br/>defect still open"]
The arrow that matters is the last one: what stopped happening was the trigger this project controls, not the defect underneath it. I’m calling that a correlation and not a cause on purpose — two players, two sessions, no recurrence is real, but nobody has proven the second placement is what actually sends the bad notification rather than something else nearby that happened to go away with it.
What I hadn’t actually checked
The name for the underlying defect, UE-16932, came from an Epic staff reply on a 2015 forum thread, cited by number: “I did some digging and found this issue has already been reported. UE-16932. We are working to address this issue and have it fixed in an upcoming release.” An earlier note on this project had already flagged that the thread’s own description — widgets responding at the wrong position — doesn’t quite match what I measured, which is a total miss rather than an offset. The identification carried forward anyway, because it kept coming from the same staff reply, not from a fresh comparison against the ticket.
So I went and read the ticket itself. UE-16932’s own record — title “MAC: Mouse input does not aligned with the cursor when switching from windowed to fullscreen,” component UE - Platform - Apple, affects 4.8.2 — lists its resolution as Fixed, target fix 4.9, resolved July 2015. That’s not what I expected going in, and it’s not nothing either: a later reply on the same public thread, dated December 2023, reads “UE 4.27.2 still have the same issue on MAC OSX, any resolution as of now?” — eight years and several major versions after the ticket the staff reply pointed at was marked fixed.
I can’t make that add up to either “the same bug came back” or “it’s a different bug.” What I can say plainly is that an earlier commit message on this project claims UE-16932 was “reported from UE 4.8.2 through 5.6 with no fix,” and I could not verify that claim myself. The 2015 report and the December 2023 reply are the two data points I could actually open and read on the forum thread the identification traces back to; I found nothing on that page, in any fetch, connecting this bug to version 5.6. That note stays wrong until someone corrects it at the source — I’m not editing it here, but I’m not repeating its unverified half either.
The shape of that is familiar: a fact written down once, carried forward through however many repetitions, and never checked against the thing it actually claims to describe until something forces the question back open.
PostWhat gets lost between iterationsI number my Unreal prototypes and start each one fresh, with its own repo and its own initial commit. That convention lost the same kind of thing three times in a row — and a fourth loss, surfaced when an agent reasoned soundly from a stale line in a doc, turned out to be a different kind of failure entirely.What I tried and walked back
Before any of the above was measured, the obvious workaround was to just not use WindowedFullscreen on macOS — run EWindowMode::Fullscreen instead, since that’s the one branch OnWindowDidMove protects unconditionally. I tried it, once, and it cost the whole machine’s keyboard.
Exclusive fullscreen is the only path that calls CGDisplayCapture (Runtime/ApplicationCore/Private/Mac/MacWindow.cpp:695) and raises the window above CGShieldingWindowLevel() (:748, :751) — a level the OS itself doesn’t draw over, including its own modal dialogs. A keychain permission prompt came up behind that window during the same run, mid-sign-in. It couldn’t be seen, couldn’t be clicked, and it held keyboard input for the entire machine — Cmd+Tab, Cmd+Q, Force Quit, all of it, until the session got forced into denying the prompt blind. A thread on Epic’s own forums describes the identical lockout independently: “When I’m using EWindowMode::Fullscreen then … any system-wide hotkeys are not working at all (e.g. Cmd+Tab, Cmd+Q, etc).” Apple’s own documentation on the technique says as much too: “It’s not necessary to capture a display to do full-screen drawing. Another approach is to create and draw into a borderless window the size of the display… you can also use Command-Tab to change applications with this approach.”
Withdrawn, and recorded in a comment in the project’s fullscreen config right next to the line it would have replaced, specifically so nobody proposes it again without reading why it went in and came back out in the same session.
Things still bugging me
The engine defect is not fixed. It’s still there whether or not the specific trigger on this one map is gone — the second-placement fix removed an event the fault reliably followed, not the mechanism that reads a mid-transition window frame and never re-checks it. If a second placement returns by some other route, or the move notification fires for any other reason at the wrong instant, this comes back.
bIsOnActiveSpace is still unmeasured — whether it goes stale at the same moment the position does is a real gap in the diagnosis, not a hypothetical one. Which of the four notifications that reach OnWindowDidMove actually fires here is still unknown. And the fix that made the symptom go quiet only asks the server whether a map is ready; a client whose own streaming lags behind the server’s isn’t covered by any of it, which matches a pattern other people have reported independently — a client falling through a map it hasn’t finished loading yet.
Wrap-up
The part of this I’ll actually carry forward isn’t the engine trace, even though that’s the part that took the longest to get right. It’s that I had accepted “known engine bug, nothing to do” as the whole explanation once, on the strength of a name someone had already written down, and the actual fix came from going back to the one thing that name never asked: what, in my own scene, was different about the one map where this happened. The engine bug was real. It just wasn’t the whole answer, and the part that was mine to fix was mine to fix regardless of whether Epic ever ships one.
References
- UE-16932 — Unreal Engine Issues and Bug Tracker
- Mac Fullscreen Mouse Position in UMG — Unreal Engine Forums
- macOS UGameUserSettings::SetFullscreenMode — system hotkeys stop responding — Unreal Engine Forums
- Apple Developer — Capturing Displays
- Unreal Engine — Travelling in Multiplayer
- The travel bug behind why this map places a player twice