diff --git a/src/App.zig b/src/App.zig index 8f749890f..15e06a03a 100644 --- a/src/App.zig +++ b/src/App.zig @@ -106,6 +106,18 @@ fn threadedWarmup() void { log.warn("font warmup thread spawn failed err={}", .{err}); } } + + // Same for the renderer's graphics API (e.g. Metal), which pays + // one-time framework initialization costs on first use. + if (comptime @hasDecl(renderer.Renderer.API, "warmup")) { + if (std.Thread.spawn( + .{}, + renderer.Renderer.API.warmup, + .{}, + )) |thr| thr.detach() else |err| { + log.warn("renderer warmup thread spawn failed err={}", .{err}); + } + } } /// Initialize the main app instance. This creates the main window, sets diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6c7432d21..1ad6b812b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -407,6 +407,23 @@ pub inline fn beginFrame( return try Frame.begin(.{ .queue = self.queue }, renderer, target); } +/// Warm up the Metal device machinery. The first Metal device query in +/// a process takes multiple milliseconds; once warm, subsequent queries +/// are effectively free. Calling this early (e.g. on a background +/// thread at app startup; Metal device queries are thread-safe) moves +/// that one-time cost off the critical path of the first surface's +/// renderer initialization. +/// +/// We deliberately do NOT cache the chosen device: the device set can +/// change at runtime (e.g. an eGPU being plugged in or removed) and +/// chooseDevice prefers removable GPUs, so every renderer init must +/// re-choose. Only the underlying framework initialization is a +/// one-time cost. +pub fn warmup() void { + const device = chooseDevice() catch return; + device.release(); +} + fn chooseDevice() error{NoMetalDevice}!objc.Object { var chosen_device: ?objc.Object = null;