Reproducible in: 4.6.3.stable.official [7d41c59c4], both the standard and the .NET build.
Both rendering drivers: Metal and Vulkan (MoltenVK), Forward+.
System information
macOS 27.0.1 (26A434), Apple M5 Pro (18 cores, 64 GB), built-in display at 120 Hz. Windowed, V-Sync enabled.
Issue description
The project below does nothing except log frame intervals from one Node2D. It still drops one long frame, 24–40 ms at 120 Hz, ~15.07 s after engine start, in every launch:
The instant is the same within ±40 ms on the standard and .NET builds, on both drivers.
In a game it lands mid-play as a visible hitch.
Where the frame goes. I timestamped the first and last _process, RenderingServer.frame_pre_draw / frame_post_draw, and an InputEventAction injected each frame and seen in _input at the next event flush.
The whole stall falls after frame_post_draw and before the next input flush. That is between iterations, in the AppKit run loop, not in scripts, physics or rendering.
On that frame the viewport's CPU render time is ~0.1 ms.
What runs there. The unified log shows, at the same instant, on the :
The "initial flush" took 16–28 ms on the main thread in 14 of 14 launches I matched against the log.
Later flushes are coalesced to ~15 s and much cheaper (1–9 ms).
Godot doesn't use this feature.
Every window is already opted out of restoration: [wd.window_object setRestorable:NO] in platform/macos/display_server_macos.mm.
GodotApplicationDelegate answers applicationSupportsSecureRestorableState: with YES.
The app-level NSPersistentUIManager still runs, and its first flush is the cost.
Cause confirmed. Turning the per-app Resume switch off through the argument domain, -ApplePersistence NO (AppKit echoes ApplePersistence=NO on stderr), removes both the StateRestoration log lines and the stall:
With the flag: 0 of 5 launches stalled at ~15 s (Metal and Vulkan, standard and .NET).
Without it: 19 of 20 launches stalled.
-ApplePersistenceIgnoreState YES does not help: 5 of 5 launches still flushed and stalled.
With the MRP below on the official standard build, Metal, 120 Hz:
control #1: long frame 35.1 ms, ending at 15.074 s | log: initial flush 22:25:12.661 -> .687 (26 ms)
control #2: long frame 36.3 ms, ending at 15.076 s | log: initial flush 22:25:58.566 -> .594 (28 ms)
-ApplePersistence NO #1, #2: no long frame near 15 s, no StateRestoration lines
A later independent re-run on the same machine gave control 33.9 ms at 15.066 s and 35.9 ms at 15.069 s. With the flag, nothing near 15 s.
Other long frames in the output (18–80 ms, irregular) are not this bug. They are waits inside RenderingServer.draw for the compositor, and they line up with system activity: the menu-bar clock's minute tick and Wi-Fi daemon log bursts.
Possible fix. Opt the process out of AppKit persistence before NSApplication finishes launching, for example early in main() in platform/macos/godot_main_macos.mm:
The registration domain is the lowest-priority defaults domain, so a user or an exported app can still turn it back on.
Every Godot window is already non-restorable, so nothing user-visible should change.
If something relies on app-level restoration, this could go behind a project setting.
Steps to reproduce
Create the three files below in an empty folder, persistent_ui_flush_repro/.
Run it from a terminal so stdout is visible:
Godot.app/Contents/MacOS/Godot --path persistent_ui_flush_repro res://main.tscn
It quits by itself after 22 s.
Observe long frame XX ms, ending at ~15.07 s after engine start.
Optional: in another terminal, run
log stream --style compact --predicate 'process == "Godot" AND category == "StateRestoration"'.
The Emptying for initial flush line lands on the same instant.
Run again with -ApplePersistence NO appended after the scene path. Before it, a bare NO would be taken as the positional scene argument.
The ~15 s long frame and the log lines are gone.
Expected: no main-thread stall from a restoration feature Godot does not use.
extends Node2D
## Prints every frame whose _process-to-_process interval exceeds LONG_MS, stamped with the engine
## clock (seconds since engine start), then quits after RUN_S. Nothing else runs in this project.
const LONG_MS := 18.0
const RUN_S := 22.0
var _last_usec := 0
func _ready() -> void:
print("driver=%s vsync=%d refresh=%.0f Hz" % [RenderingServer.get_current_rendering_driver_name(),
DisplayServer.window_get_vsync_mode(), DisplayServer.screen_get_refresh_rate()])
func _process(_delta: float) -> void:
var now := Time.get_ticks_usec()
if _last_usec > 0 and now - _last_usec > LONG_MS * 1000.0:
print("long frame %.1f ms, ending at %.3f s after engine start" % [(now - _last_usec) / 1000.0, now / 1e6])
_last_usec = now
if now > RUN_S * 1e6:
get_tree().quit()