ALWAYS use to add MetalFX frame interpolation with a dedicated PresentThread for precise dual-frame presentation to a Metal app. Use this skill when the user wants to add frame interpolation, frame generation, double the framerate, or mentions MTLFXFrameInterpolator (Metal 3) or MTL4FXFrameInterpolator (Metal 4). This skill assumes temporal upscaling is already integrated (use using-metalfx-temporal-upscaler first if not). Also trigger when the user says "add frame gen", "120fps", "interpolate frames", or wants smoother motion.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
ALWAYS use to add MetalFX frame interpolation with a dedicated PresentThread for precise dual-frame presentation to a Metal app. Use this skill when the user wants to add frame interpolation, frame generation, double the framerate, or mentions MTLFXFrameInterpolator (Metal 3) or MTL4FXFrameInterpolator (Metal 4). This skill assumes temporal upscaling is already integrated (use using-metalfx-temporal-upscaler first if not). Also trigger when the user says "add frame gen", "120fps", "interpolate frames", or wants smoother motion.
Adding MetalFX Frame Interpolation to a Metal App
This skill adds MetalFX frame interpolation to a Metal app that already has a MetalFX temporal scaler integrated. The interpolator generates intermediate frames between real rendered frames, effectively doubling the display framerate (e.g., 60fps render → 120fps display).
Two API variants exist:
Metal 3 — MTLFXFrameInterpolator (macOS 15.0+ / iOS 18.0+)
Metal 4 — MTL4FXFrameInterpolator (macOS 26.0+ / iOS 26.0+), which inherits from the shared MTLFXFrameInterpolatorBase protocol
All per-frame properties are identical across both APIs (they live on MTLFXFrameInterpolatorBase). The differences are the creation call, the command buffer type used for encoding, and the support check — see the notes below.
Prerequisite: The app must already have MetalFX temporal upscaling working. If not, use the using-metalfx-temporal-upscaler skill first.
Abstraction Strategy
Before starting integration, check if the engine already has a high-level abstraction for frame interpolation (e.g., from DLSS Frame Generation, FSR Frame Gen). If it does, integrate MetalFX through the existing abstraction — follow the same execution paths wherever the APIs are compatible. This makes debugging easier and minimizes the chance for bugs. Only diverge from the existing patterns where MetalFX API differences require it.
If no existing frame interpolation abstraction exists, ask the developer: introduce a minimal abstraction layer (Recommended — cleaner, easier to maintain and debug), or implement MetalFX directly in the source without abstractions (faster, but harder to extend later)?
Important: When bridging a row-vector matrix library (DirectXMath for example) to Metal's column-vector simd::float4x4, the matrix must be transposed
Architecture
Frame interpolation uses a dedicated present thread with precise pacing for smooth output:
interpolator.depthTexture = depthTarget;
interpolator.motionTexture = motionTarget;
// Before assigning jitterOffsetX/Y, consult with the rest of the pipeline// (temporal upscaling if present) whether this is necessary and in which// space the jitter needs to be defined at that stage.
interpolator.jitterOffsetX = jitterX;
interpolator.jitterOffsetY = jitterY;
interpolator.motionVectorScaleX = renderWidth * 0.5f;
interpolator.motionVectorScaleY = renderHeight * 0.5f;
Note: MTLFXFrameInterpolator does NOT have a reset property. History management is internal.
Step 3: Modify the render flow
Old flow (temporal upscaling only):
Render scene to offscreen
Temporal upscale → _upscaledOutput
Blit _upscaledOutput to drawable
Present drawable
New flow (with frame interpolation):
[_presentThread startFrame:commandBuffer] — sync with present thread
Render scene to offscreen (unchanged)
Temporal upscale → [_presentThread getBackBuffer] (NOT your own output texture)
Commit the command buffer (PresentThread needs the GPU work done)
Set per-frame interpolator properties (depth, motion, jitter, MV scale)
The PresentThread uses a sophisticated dual-thread model for precise frame pacing:
PresentThreadFunction (encoding thread):
Waits for work from Present()
Acquires drawable, copies interpolated frame from m_interpolationOutputs, presents
Acquires second drawable, copies real frame from m_backBuffers, presents
Uses MTLEvent to ensure GPU work ordering
PacingThreadFunction (pacing thread):
Uses kqueue + kevent64 with NOTE_MACHTIME | NOTE_ABSOLUTE for sub-ms timer precision
Calculates timestamp at ~48% of frame interval: time + (delta * 31) >> 6
Signals MTLSharedEvent to gate the second present
Present() (called from render thread):
Configures interpolator's colorTexture (current back buffer) and prevColorTexture (previous back buffer)
Encodes interpolation to command buffer
Manages frame-in-flight count (max 2)
Advances triple buffer index
Texture Usage Flags
All PresentThread textures:
Back buffers: ShaderRead | ShaderWrite | RenderTarget
Interpolation outputs: ShaderRead | RenderTarget
Interpolation inputs: ShaderRead | RenderTarget
All use MTLPixelFormatRGBA16Float and StorageModePrivate
Troubleshooting
Crash on launch (zero-size textures): Add zero-dimension guard in Resize()
Deadlock: Ensure Present() is called from the render thread, not the present thread
Black frames: Verify temporal scaler output goes to getBackBuffer(), not a separate texture
Frame interpolator returns nil: Check OS version and device support. Metal 3: +[MTLFXFrameInterpolatorDescriptor supportsDevice:] (macOS 15.0+ / iOS 18.0+). Metal 4: +[MTLFXFrameInterpolatorDescriptor supportsMetal4FX:] (macOS/iOS 26.0+) — a distinct class method that checks Metal 4 compatibility.
@import errors in .mm file: Use #import <Metal/Metal.h> etc. in the header, not @import
Verification
Confirm the integration from debug logs:
Frame interpolation — debug logs confirm interpolation is happening each frame
The developer must confirm the debug log verification before the integration is considered complete.
Visual Verification
After building and launching the app, verify rendering is working by capturing a screenshot of the app window. If possible closest match vs non-upscaled. This is useful both for self-checking your work and for showing the user what the result looks like.
Capture the app window (not the full screen — just the window):
# Find the main content window by app name, filtering for the largest window
APP_NAME="MyApp"# Replace with the actual app/process name
WID=$(python3 -c "
import Quartz
for w in Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionAll, Quartz.kCGNullWindowID):
if '$APP_NAME' in w.get('kCGWindowOwnerName', ''):
b = w.get('kCGWindowBounds', {})
if b.get('Height', 0) > 100:
print(w['kCGWindowNumber']); break
")
screencapture -x -o -l "$WID" /tmp/mfx_screenshot.png
Then view the screenshot at /tmp/mfx_screenshot.png. Look for:
The 3D scene is visible (not black, not a blank window)
Motion looks smooth — with frame interpolation, camera pans and object movement should appear fluid
No obvious artifacts like ghosting on moving objects or tearing between interpolated and real frames