Emilio Melis / aatricks
  • build 2026.06
  • stack · 5 components
  • published 2026-06-27

FrameLimiter

A frame cap that actually does less GPU work, not just shows fewer frames. It paces the one call every Metal game goes through and lets back-pressure do the rest.

01Platform
Apple Silicon · native Metal
02Injection
DYLD_INSERT_LIBRARIES dylib
03Pacing
mach_wait_until · sleeps, never spins
04Control
live-tunable fps file · no restart

why it matters

  • On a fanless Apple Silicon laptop, running way above what the screen shows just drains the battery and makes heat for frames you never see. The cap stops that.
  • A limiter can only cap downward. Asking for more than the refresh only helps if the game already renders that fast with VSync off.
  • The cap lives in a little file the dylib watches, so flctl can change it or turn the limiter off mid-game, no restart.
  • For Steam it installs by adding an LSEnvironment block to the game's Info.plist and re-signing, which keeps the real executable so Game Mode and the over-60 Hz path still work.

engineering notes

FrameLimiter is a frame-rate limiter for native Metal games on Apple Silicon. It’s a small dylib you inject with DYLD_INSERT_LIBRARIES that caps a game’s frame rate to whatever target you want, separate from the game’s own VSync, so you can trade input lag for less GPU power and heat.

One hook

A game gets each frame by asking its CAMetalLayer for the next drawable. FrameLimiter hooks -[CAMetalLayer nextDrawable] (method swizzling) and paces that one call with mach_wait_until. Basically every native Metal game goes through that method, whether it’s raw Metal, MetalKit/MTKView, SDL2, or MoltenVK, so one hook covers all of them. It sleeps until the next frame is due instead of busy-looping, because busy-looping would burn the power the cap is trying to save.

Back-pressure

Pacing nextDrawable does more than just delay frame presentation. Holding the call back drains the drawable pool and stalls the render thread, which applies back-pressure so the GPU stops rendering new frames. On a fanless laptop, running at 200+ FPS just wastes battery and makes the machine hot for frames the screen can’t even show.

Adaptive VSync and live control

A limiter can only cap downward. If your target is above the display refresh, FrameLimiter forces displaySyncEnabled off so the game can run faster than the panel. That tears on a fixed-refresh screen, which is the trade for lower lag; at or below the refresh it leaves the game’s VSync alone. The cap itself lives in a small file the dylib watches, so flctl can change it, disable it, or toggle it while the game runs without a restart.

Steam on macOS can’t pass environment variables through launch options, so installing per game means touching the game bundle. FrameLimiter adds an LSEnvironment block to the game’s Info.plist and ad-hoc re-signs it, so the system injects the dylib when the game launches as itself. I could’ve just replaced the executable, but that makes macOS stop recognizing the app, which kills Game Mode and the direct-to-display path that lets a game go past 60 Hz. Editing Info.plist keeps the app’s identity, so those keep working.