Hardware-Paced Metal Frame Rate Limiter for NotProton & macOS Wine
A lightweight, universal frame pacing plugin for NotProton.
Eliminates micro-stutter and delivers flat frametimes in DirectX & Vulkan games running on macOS via D3DMetal, DXMT, and DXVK.
Deadlock (DirectX 11 via D3DMetal) locked at 30 FPS on a 120Hz ProMotion display — flat 33.75ms frametime line on the Metal HUD.
Important
Compatibility: Requires macOS 14.0+ and NotProton. Built as a native universal fat binary supporting both Apple Silicon (M1/M2/M3/M4) and Intel Macs.
- Flat Frametime Delivery: Meters frame submission intervals and paces CoreAnimation presentation to eliminate judder and uneven display pulldown.
-
Hybrid Precision Pacing: Combines high-resolution kernel waits (
mach_wait_until) with microsecond spin-yields (CPU_YIELD) onQOS_CLASS_USER_INTERACTIVEthreads. - Bresenham Remainder Tracking: Accumulates fractional nanosecond clock remainders across frames to guarantee zero drift over multi-hour gaming sessions.
-
Native Steam UI Integration: Adds a clean slider directly into your game's Properties
$\to$ Compatibility tab in Steam (snapping toOff,30,40,60,120, and144FPS). - Optional VSync Decoupling: Includes an optional sub-toggle to disable VSync quantization when immediate presentation is preferred.
- Zero Dependencies: Pure C and Objective-C linking directly against system Metal and QuartzCore frameworks — minimal footprint, zero external overhead.
When running Windows games on macOS through translation layers (such as D3DMetal or DXMT), games often exhibit micro-stutter even when the game's internal frame counter reports a steady 60 or 30 FPS:
Unpaced Frame Delivery (Judder & Micro-stutter):
Frame 1: |-- 33.3ms --|
Frame 2: |-- 33.5ms (slipped past vblank by 0.2ms!) ------> held to 41.7ms (5 vblanks)
Frame 3: |-- 25.0ms (catchup) --|
With notproton-fpslimit (Flat Cadence):
Frame 1: [33.3ms - 4 vblanks]
Frame 2: [33.3ms - 4 vblanks]
Frame 3: [33.3ms - 4 vblanks]
Without presentation duration awareness, if a frame finishes rendering even 0.1ms past a vertical refresh boundary on a 60Hz or 120Hz ProMotion display, macOS CoreAnimation holds that frame for an entire extra vblank. The result is irregular 3:2 or 5:3 pacing spikes.
notproton-fpslimit solves this by:
- Pacing CPU frame submission ahead of
CAMetalLayer nextDrawableusing Mach absolute time, interactive QoS priority, and Bresenham fractional clock tracking. - Eliminating swapchain starvation and input latency by letting CoreAnimation and Metal present naturally without artificial compositor holds.
Run this command in Terminal:
curl -fsSL https://raw.githubusercontent.com/Ghostkwebb/notproton-fpslimit/main/install.sh | bash- Download
notproton-fpslimit.zipfrom Releases. - Unzip it and copy the files to your NotProton directory:
- Copy
libfpslimit.dylib$\to$ ~/Library/Application Support/notproton/ - Copy
fpslimit.conf$\to$ ~/Library/Application Support/notproton/plugins/
- Copy
- Restart Steam.
- Open Steam.
- Right-click any game running via NotProton
$\to$ Properties$\to$ Compatibility. - Under the Developer section, adjust the FPS Limiter slider:
-
Off: Unconstrained frame rate -
30 FPS: Locked 4 vblanks on 120Hz ProMotion / 2 vblanks on 60Hz -
40 FPS: Locked 3 vblanks on 120Hz ProMotion (ideal handheld / battery sweet spot) -
60 FPS: Locked 2 vblanks on 120Hz ProMotion / 1 vblank on 60Hz -
120 FPS/144 FPS: High refresh rate caps
-
- Launch your game.
(Optional) You can also set a custom FPS limit via Steam launch options:
NOTPROTON_FPS_LIMIT=45 %command%
- macOS 14.0+ (Sonoma or Sequoia)
- Xcode Command Line Tools (
xcode-select --install)
git clone https://github.com/Ghostkwebb/notproton-fpslimit.git
cd notproton-fpslimit
makemake installmake uninstall- Layer Hooking: Swizzles
CAMetalLayer nextDrawablevia Objective-C runtime method swapping. - Swapchain Integrity: Paces CPU frame requests before calling into the driver, avoiding swapchain starvation and input queue delays without intrusive compositor holds.
- Thread-Local State: Uses
_Thread_localreentrancy guards to prevent swizzle recursion, and escalates pacing threads toQOS_CLASS_USER_INTERACTIVE. - Architecture Agnostic: Implements CPU yields via
__builtin_arm_yield()on Apple Silicon (ARM64) and_mm_pause()on Intel (x86_64).
Distributed under the MIT License. See LICENSE for more information.
Crafted natively for macOS Gaming