From 131b293dbbf426acf57618bc43bef0a4fe260d12 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 15:43:31 -0700 Subject: [PATCH] renderer/metal: warm up the Metal device machinery at app creation The first Metal device query in a process (MTLCopyAllDevices) takes multiple milliseconds; once the framework is warm, subsequent queries are effectively free (measured ~15ms cold, ~1us warm in isolation). This cost was paid during the first surface's renderer initialization, on the critical path to the first window. Measured on macOS (Apple Silicon) with local timing instrumentation during app launch, first surface renderer initialization: GraphicsAPI.init before: 4227us (device query ~3.5ms) GraphicsAPI.init after: ~900us (device query 20-25us) --- src/App.zig | 12 ++++++++++++ src/renderer/Metal.zig | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) 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;