renderer/metal: warm up command queue and shader pipelines

Extend the Metal portion of the startup warmup thread to also create
(and discard) a command queue and build (and discard) the shader
pipelines for both pixel formats we may use (which one is used
depends on the blending config). The first command queue for a device
and the first render pipeline state creations pay one-time driver
setup and shader compilation costs; once warm, the real creations
during surface initialization hit driver and OS caches.

Measured on macOS (Apple Silicon) with local timing instrumentation
during app launch, first surface renderer initialization:

  queue creation:  717us -> 93us
  pipeline builds: 1023us -> 347us
  renderer init total: 2777us -> 1466us
This commit is contained in:
Mitchell Hashimoto
2026-08-09 15:44:25 -07:00
parent 131b293dbb
commit de1336fadd

View File

@@ -421,7 +421,34 @@ pub inline fn beginFrame(
/// one-time cost.
pub fn warmup() void {
const device = chooseDevice() catch return;
device.release();
defer device.release();
// Create and release a command queue. The first command queue
// created for a device pays additional one-time driver setup
// costs; subsequent creations are much cheaper.
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
queue.release();
// Build and discard our shader pipelines for both pixel formats we
// may use (which one is used depends on the blending config). The
// first pipeline state creation compiles shaders which is slow;
// once warm, later creations hit driver and OS caches.
inline for (.{
mtl.MTLPixelFormat.bgra8unorm_srgb,
mtl.MTLPixelFormat.bgra8unorm,
}) |format| {
if (shaders.Shaders.init(
std.heap.c_allocator,
device,
&.{},
format,
)) |s| {
var s_mut = s;
s_mut.deinit(std.heap.c_allocator);
} else |err| {
log.warn("metal warmup shader init failed err={}", .{err});
}
}
}
fn chooseDevice() error{NoMetalDevice}!objc.Object {