From 79d14837627260aa8065d10c1abb7500a47218bc Mon Sep 17 00:00:00 2001 From: foxnne Date: Tue, 13 Oct 2026 09:32:14 -0500 Subject: [PATCH] app: a window shows once it has drawn fizzy's window used to be shown at the end of AppInit, before its first frame, so it appeared empty for the ~80 ms that frame took (#365's phases measured it), and in a restart's handover the new window covered the old one before it had drawn. Now it is shown unseen (macOS: transparent; Windows: DWM-cloaked, not layered), draws its first frame, and is revealed as the next frame begins. A handover's new instance signals ready only once it is on screen. Co-Authored-By: Claude Opus 5.5 --- app/restart.zig | 9 +++-- backend/src/platform/window.zig | 34 +++++++++++++++++++ src/Entry.zig | 58 +++++++++++++++++++-------------- src/backend/backend_native.zig | 8 +++++ src/backend/backend_web.zig | 2 ++ 5 files changed, 83 insertions(+), 28 deletions(-) diff --git a/app/restart.zig b/app/restart.zig index 479aa416b..939864cf8 100644 --- a/app/restart.zig +++ b/app/restart.zig @@ -248,9 +248,9 @@ pub fn startedByHandover() bool { return handed_from != null; } -/// The new instance, up and hidden: tell the old one, which quits. The caller shows its window now, -/// over the old one's at the same place, so a window is on screen throughout; the lock is taken -/// once the old one has let go of it (`pollReleased`). +/// The new instance, its window on screen over the old one's at the same place: tell the old one, +/// which quits, so a window is on screen throughout; the lock is taken once the old one has let go +/// of it (`pollReleased`). pub fn signalReady(io: std.Io) void { if (comptime !supported) return; const dir = handed_from orelse return; @@ -269,6 +269,9 @@ var released_deadline_ns: i96 = 0; pub fn pollReleased(io: std.Io, gpa: std.mem.Allocator) bool { if (comptime !supported) return false; const dir = handed_from orelse return false; + // Not yet ready (`signalReady`, once the window is on screen): the old instance still holds + // the lock, and has not been asked to let go of it. + if (released_deadline_ns == 0) return false; var buf: [std.fs.max_path_bytes]u8 = undefined; const released = std.fmt.bufPrint(&buf, "{s}/released", .{dir}) catch return false; const done = if (std.Io.Dir.cwd().access(io, released, .{})) |_| true else |_| std.Io.Clock.awake.now(io).nanoseconds > released_deadline_ns; diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index 485aecec4..488df2bd1 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -158,6 +158,40 @@ pub fn show(win: *dvui.Window, maximized: bool) void { if (maximized) _ = c.SDL_MaximizeWindow(win.backend.impl.window); } +/// Show the window without it being seen yet, so it can draw its first frame before anyone +/// looks (`reveal`). A hidden window gets nothing to present into, so it cannot draw first and show +/// after; shown but unseen, it can. On macOS the window is shown fully transparent (`alphaValue`); +/// on Windows it is cloaked (`DWMWA_CLOAK`), shown and composited but not on screen, which keeps it +/// out of layered windows (see `win32_titlebar`). Elsewhere it shows as it is, and `reveal` does +/// nothing. +pub fn showUndrawn(win: *dvui.Window, maximized: bool) void { + const window = win.backend.impl.window; + switch (builtin.os.tag) { + .macos => _ = c.SDL_SetWindowOpacity(window, 0), + .windows => setCloaked(win, true), + else => {}, + } + show(win, maximized); +} + +/// The first frame is on screen: let it be seen (`showUndrawn`). +pub fn reveal(win: *dvui.Window) void { + switch (builtin.os.tag) { + .macos => _ = c.SDL_SetWindowOpacity(win.backend.impl.window, 1), + .windows => setCloaked(win, false), + else => {}, + } +} + +fn setCloaked(win: *dvui.Window, cloaked: bool) void { + if (comptime builtin.os.tag != .windows) return; + const hwnd = win32_titlebar.getWin32Hwnd(win) orelse return; + const win32 = @import("win32"); + const value: u32 = @intFromBool(cloaked); + // DWMWA_CLOAK. + _ = win32.graphics.dwm.DwmSetWindowAttribute(@ptrCast(hwnd), @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(13)), &value, @sizeOf(u32)); +} + pub fn isMaximized(win: *dvui.Window) bool { return windowMaximized(win.backend.impl.window); } diff --git a/src/Entry.zig b/src/Entry.zig index ba736cfbe..e89773e4b 100644 --- a/src/Entry.zig +++ b/src/Entry.zig @@ -408,32 +408,39 @@ pub fn AppInit(win: *dvui.Window) !void { // window animations. fizzy.backend.macosLaunchComplete(); - // Started by a restart's handover: up and hidden, the session read. The old instance goes - // now, and this window shows over its own at the same place (`restart`); the lock follows - // once the old one lets go of it (`Editor.tick`). - if (restart.startedByHandover()) { - restart.signalReady(dvui.io); - fizzy.core.profile.phase(dvui.io, "handover ready"); - } - - // Chrome and geometry are settled — reveal the window (created hidden). - fizzy.backend.showWindow(win); - fizzy.core.profile.phase(dvui.io, "shown"); -} - -/// The first frame has ended: the app is up. Says how long that took, once. -var first_frame_done = false; -fn noteFirstFrame() void { - if (first_frame_done) return; - first_frame_done = true; - fizzy.core.profile.phase(dvui.io, "first frame"); - const ph = fizzy.core.profile.phases() orelse return; - std.log.info("startup: up {d:.0} ms after main (window shown at {d:.0} ms)", .{ ph.ms(ph.len - 1), shownMs(ph) }); + // Chrome and geometry are settled: the window shows, but unseen until its first frame has + // reached the screen (`revealOnceDrawn`), so it never appears as an empty window first. + fizzy.backend.showWindowUndrawn(win); } -fn shownMs(ph: *const fizzy.core.profile.Phases) f64 { - for (0..ph.len) |i| if (std.mem.eql(u8, ph.names[i], "shown")) return ph.ms(i); - return 0; +/// Where the window's reveal is (`revealOnceDrawn`): drawing its first frame, then seen. +var reveal: enum { first_frame, next_frame, done } = .first_frame; + +/// At each frame's start, then its end: the first frame is drawn into the unseen window and +/// presented as the frame ends, so the window is revealed as the next one begins, which the +/// first asks for. Then a restart's handover tells the old instance this one is ready, now that it +/// is on screen over the old one's window at the same place (`restart`); the lock follows once +/// the old one lets go of it (`Editor.tick`). +fn revealOnceDrawn(at_end: bool) void { + switch (reveal) { + .first_frame => if (at_end) { + fizzy.core.profile.phase(dvui.io, "first frame"); + reveal = .next_frame; + dvui.refresh(null, @src(), null); + }, + .next_frame => if (!at_end) { + fizzy.backend.revealWindow(fizzy.entry().window); + fizzy.core.profile.phase(dvui.io, "shown"); + reveal = .done; + if (restart.startedByHandover()) { + restart.signalReady(dvui.io); + fizzy.core.profile.phase(dvui.io, "handover ready"); + } + const ph = fizzy.core.profile.phases() orelse return; + std.log.info("startup: shown {d:.0} ms after main", .{ph.ms(ph.len - 1)}); + }, + .done => {}, + } } // Run as app is shutting down before dvui.Window.deinit() @@ -468,7 +475,8 @@ pub fn AppFrame() !dvui.App.Result { _ = dvui.debug.logRefresh(refresh_log_active or fizzy.core.profile.recordingCauses()); fizzy.core.profile.hostFrameBegin(lastSubmitNs()); defer fizzy.core.profile.hostFrameEnd(); - defer noteFirstFrame(); + revealOnceDrawn(false); + defer revealOnceDrawn(true); singleton.drainPending(); // Once, or — while a demo is seeking — again and again unseen until it lands (`frames`). const player = &fizzy.editor().demo.player; diff --git a/src/backend/backend_native.zig b/src/backend/backend_native.zig index a4af6dd4a..18072aba9 100644 --- a/src/backend/backend_native.zig +++ b/src/backend/backend_native.zig @@ -106,6 +106,14 @@ pub fn showWindow(win: *dvui.Window) void { platform.window.show(win, platform.geometry.restoredMaximized()); } +/// Show the window unseen, to draw its first frame before it is seen (`revealWindow`). +pub fn showWindowUndrawn(win: *dvui.Window) void { + platform.window.showUndrawn(win, platform.geometry.restoredMaximized()); +} + +/// Its first frame is on screen: let it be seen. +pub const revealWindow = platform.window.reveal; + /// Style the window (macOS: frame == content first), put it back where it was left /// (`platform.geometry`, kept in fizzy's `layout.zon`), and follow it through Spaces, zooms and /// live resizes (`platform.macos_monitor`). Called from `AppInit` while the window is still hidden: diff --git a/src/backend/backend_web.zig b/src/backend/backend_web.zig index 69a5a2cbc..2783c4280 100644 --- a/src/backend/backend_web.zig +++ b/src/backend/backend_web.zig @@ -121,6 +121,8 @@ pub fn restoreWindowState(_: *dvui.Window) void {} /// Symmetric with the native API: the web canvas is always visible. pub fn showWindow(_: *dvui.Window) void {} +pub fn showWindowUndrawn(_: *dvui.Window) void {} +pub fn revealWindow(_: *dvui.Window) void {} /// Symmetric with the native API: no window geometry to persist on web. pub fn saveWindowGeometry(_: *dvui.Window) void {}