Experimental direct OpenGL-to-Metal presentation backend for Psychtoolbox on Apple Silicon

Motivation

I have been investigating the presentation-timing problems affecting Psychtoolbox on Apple Silicon under recent macOS versions. In particular, ordinary drawing and unscheduled flips can appear to work, while some scheduled presentation tests exhibit half-rate or irregular behavior.

As an experiment, I created a small component called PsychMetal. It retains PTB’s normal Screen drawing functions but replaces window creation, matrix-to-texture creation, presentation, and cleanup with a direct macOS Metal presentation path.

Basic interface

The supported interface is deliberately small:

[w, rect, ifi] = PsychMetal('OpenWindow', screenNumber);
texture = PsychMetal('MakeTexture', w, imageMatrix);
vbl = PsychMetal('Flip', w);
history = PsychMetal('Diagnostic', w);
PsychMetal('Close', w);

All actual drawing continues to use the normal PTB Screen functions with the returned handle w. This includes shapes, dots, text, textures, rotation, blending, and other ordinary drawing operations.

For a basic PTB program, the required substitutions are:

Screen('OpenWindow', ...)   -> PsychMetal('OpenWindow', ...)
Screen('MakeTexture', ...)  -> PsychMetal('MakeTexture', ...)
Screen('Flip', ...)         -> PsychMetal('Flip', ...)
Screen('CloseAll')          -> PsychMetal('Close', w)

For example:

[w, rect, ifi] = PsychMetal('OpenWindow', max(Screen('Screens')));

Screen('FillRect', w, [128 128 128]);
Screen('DrawDots', w, xy, sizes, colors);

vbl = PsychMetal('Flip', w);

history = PsychMetal('Diagnostic', w);
PsychMetal('Close', w);

Textures returned by PsychMetal('MakeTexture') are ordinary PTB texture handles and are drawn and closed normally:

texture = PsychMetal('MakeTexture', w, imageMatrix);
Screen('DrawTexture', w, texture, [], destination, angle);
Screen('Close', texture);

Presentation path

PsychMetal creates two IOSurfaces that are shared between OpenGL and Metal. These surfaces are attached alternately to the OpenGL framebuffer used by PTB drawing commands.

After drawing, the completed IOSurface is presented through CAMetalDisplayLink in a native fullscreen Metal window. The other IOSurface is immediately attached as the next drawing buffer.

The resulting path is approximately:

PTB Screen drawing through OpenGL
             ↓
double-buffered IOSurfaces
             ↓
native Metal presentation

PsychMetal does not call Screen('Flip') or use PTB’s Vulkan/MoltenVK presentation path. The OpenGL framebuffer attachment is the IOSurface subsequently sampled by Metal, so there is no separate full-frame OpenGL copy into an intermediate surface. Metal performs the final IOSurface-to-drawable rendering pass.

The returned drawing rectangle is the physical Retina framebuffer size rather than a smaller logical resolution that is subsequently enlarged.

Why Flip returns a projected timestamp

One important difference concerns timestamps.

To sustain one presentation per refresh, Metal keeps frames queued in advance. On my system, the display link normally accepts a submitted frame with a target approximately two refresh intervals in the future.

At that point the target presentation time is known, but the actual presentedTime does not yet exist because the frame has not been displayed. Metal supplies the confirmed timestamp through a callback only after the future presentation occurs.

PsychMetal('Flip', w) therefore returns the calibrated projected presentation time. It does not wait for the later confirmed timestamp.

Waiting inside every Flip for the actual timestamp would stall the MATLAB or Octave loop while the queued frame crosses those future refreshes. This would prevent subsequent frames from being submitted sufficiently far in advance and could reduce the presentation rate. Confirmed timestamps are therefore collected asynchronously.

Diagnostic timestamp history

After or outside the presentation loop:

history = PsychMetal('Diagnostic', w);

returns one record for every issued Flip, including:

history.flipNumber
history.frameID
history.projectedTimestamp
history.actualTimestamp
history.actualStatus
history.targetErrorMs
history.projectionLeadMs
history.scheduledAt
history.confirmationCallbackTime
history.confirmationDelayMs
history.displayLinkTick
history.commandStatus

The projected timestamp returned during presentation can therefore be compared with Metal’s subsequently confirmed timestamp for the same Flip.

Occasionally Apple returns no valid presentedTime for a frame. PsychMetal records the actual timestamp as unavailable in that case. A missing timestamp is not automatically classified as a dropped physical frame, particularly when the projected timestamps and display-link frame counts remain consecutive.

Results so far

On an M4 MacBook Air with a 60 Hz display, a 3,600-frame fullscreen test produced:

  • 3,600 valid scheduled presentations

  • median presentation interval of approximately 16.6667 ms

  • 99th-percentile interval of approximately 16.6668 ms

  • no skipped refreshes in that run

  • approximately 1.2 ms median drawing time

  • approximately 13 ms median synchronous Flip-call duration

The packaged binaries were tested under both native ARM64 GNU Octave and native ARM64 MATLAB.

Repeated opening, presentation, cleanup, reopening, and MEX unloading were also tested. Two consecutive MATLAB runs each produced 30 valid presentations, 29 consecutive frame intervals, and no skipped refreshes.

These remain software timestamp tests. Independent photodiode validation is required before relying on the timestamps for experimental stimulus timing.

Current limitations

PsychMetal deliberately implements only a restricted feature set:

  • Apple-silicon macOS

  • native fullscreen presentation

  • selection of an attached PTB screen number

  • 8-bit monoscopic drawing

  • one new presentation per refresh

  • ordinary PTB drawing commands

  • textures created from MATLAB or Octave matrices

  • projected timestamps during presentation

  • asynchronously collected confirmed timestamps

It does not currently implement:

  • the when argument

  • dontclear or dontsync

  • asynchronous Flip

  • multiflip

  • multiple-refresh presentation intervals

  • stereo

  • HDR

  • DataPixx or other specialized display hardware

  • the full functionality of Screen('Flip')

Source and download

Source repository:

Packaged experimental release:

The repository is MIT-licensed and includes:

  • PsychMetal.m

  • the complete Objective-C++ MEX source

  • precompiled ARM64 binaries for MATLAB and Octave

  • a Makefile and build scripts

  • an original direct-presentation demonstration

  • installation, conversion, diagnostic, and build documentation

It does not include or redistribute Psychtoolbox source code. Psychtoolbox is an external dependency.

After downloading, add the extracted PsychMetal directory to the MATLAB or Octave path:

addpath('/path/to/PsychMetal')

Then test it with:

PsychMetalDirectDemo(600)

The precompiled binary for the current host should be selected automatically. Both binaries can also be rebuilt from source:

make all

or separately:

make octave
make matlab

Because a browser may apply a macOS quarantine attribute to downloaded MEX files, a user who trusts the downloaded source and binaries may need to run:

cd /path/to/PsychMetal
xattr -dr com.apple.quarantine .

Scope, intent, and possible future direction

This experiment is not intended to replace or compete with Psychtoolbox. PTB provides a mature, cross-platform system with a much broader collection of drawing, timing, input, audio, imaging, and specialized-hardware functionality.

I also understand the importance of maintaining one coherent cross-platform implementation rather than developing and supporting independent versions of PTB for macOS, Linux, and Windows.

The difficulty is that the preferred modern graphics API is now platform-dependent. Metal is native to macOS, Vulkan is available natively on Linux and Windows, and Direct3D is native to Windows. Vulkan applications can run on macOS through MoltenVK, but that introduces a Vulkan-to-Metal translation layer—the layer this experiment deliberately bypasses for presentation.

PsychMetal is therefore not a proposal for a separate macOS implementation of the whole toolbox. I am simply experimenting with the smallest practical direct Metal presentation path to see whether it can provide reliable frame pacing and useful timestamps while retaining PTB’s existing drawing interface.

The current implementation still relies on PTB’s OpenGL drawing functions. Apple deprecated OpenGL several years ago and may eventually remove it. A possible longer-term architecture could preserve one common, user-facing PTB drawing API while implementing its lower-level operations through platform backends—for example, Vulkan on Linux and Windows and Metal on macOS. Textures, shapes, dots, text, rotation, blending, and antialiasing could then behave consistently even though their native implementations differed.

That would be a substantially larger undertaking. For now, this is a focused experiment and diagnostic tool, shared in case the implementation or timing results are useful for understanding the current macOS behavior or informing future backend work.

Interesting. How does this tally with the observation you made previously that you needed to re-run a PTB demo a few times (I think 2 or 3?, I forget). then macOS seemed to behave sensibly.

Also, no source code?

Yes, the source code is on the GitHub link I attached.

Keith

Hi Peter, that behavior is gone as of MacOS 27 (Golden Gate), at least the most recent developer beta. Now the demos all work fine with no frame-rate halving. But the “when” parameter of the Screen(‘Flip’) command is still broken.

I’ve been playing around with Metal more and I’ve implemented the “when” parameter to PsychMetal (will upload update in a couple days).

One thing that I’ve discovered (and these are personal discoveries because I’m sure that Mario is well aware of all this), is that accurate time stamps cannot be returned at the time of the flip, because MacOS requires you to schedule three frames in advance (or 2 frames in advance if you have Game Mode enabled). So, at the time of the flip call, the best it can do is to return an estimated presentation time, and you have to retrieve the actual timestamp later.

In terms of presenting pre-determined frame-accurate stimuli, I think this will work. If you have some sort of contingent display, the best you can do is a lag of 2 frames.

Keith

Awesome. Thanks for the reply. Good to know the frame issue is fixed in Golden Gate (or at least seems to be). I have not played around with Metal before for stimulus presentation, just GPU compute.

My apologies!, I missed the source file.

Happy to test anything you want if at all helpful. I am still running Tahoe. Easiest would be to ping me by email.

How have you found Golden Gate so far? It seems to be being billed as a massive bug fix and under the hood improvement / efficiency release. Which would be good for sure.

P

Hi Peter, that behaviour is gone as of MacOS 27 (Golden Gate), at least the most recent developer beta. Now the demos all work fine with no frame-rate halving. But the “when” parameter of the Screen(‘Flip’) command is still broken.

Interesting. You mean regular Psychtoolbox demos now work at full framerate?

On vacation I have my small M1 MacBook Air, which resides and travels with my girlfriend. I upgraded it to the latest macOS 26.6.1 Tahoe, tested with Octave (no Matlab license available right now), and found it to be even more broken than the earlier 26.2/3 Tahoe.

I got everything from 1 second stalls, not showing anything on the screen (discarding every single frame), running at full refresh rate while totally ignoring the ‘twhen’ parameter, returning completely made up timestamps, to one (!) run where everything worked once wrt. timing and timestamping, just to break then again. Nothing I tried made it any better.

Then I gave up and remembered I’m on my first real vacation in over a year on a beautiful greek island and swimming, reading, eating and drinking cocktails is much more fun. :smiling_face_with_sunglasses:

So this improved significantly?

PTB’s Vulkan backend with no twhen does schedule for present at next refresh, and at least in direct-to-display fullscreen mode under the right conditions one should be able to present without 2-3 frames latency. At least some tests with earlier macOS versions did show that.

It also waits for real timestamp availability, and before macOS 26 those timestamps did become available within ~1 msec after present completion. All the most horrible trouble started with macOS 26.

So if regular PTB demos can run at full refresh and provide rather noise-free timestamps (noise in the low single-digit microsecond range), that would be an improvement for macOS 27.

Your implementation could be useful for me as another reference, to see if it behaves different from the Vulkan/MoltenVK implementation. The internal approach is almost identical to what we do now:

  1. Screen’s framebuffer attachment to the imaging pipeline is a IOSurface, which is represented (=acts as backing storage for) by a OpenGL color texture on the Screen side, and as VKImage on the PsychVulkanCore side. No copies are performed, it is zero copy on the interop side.
  2. Internally our Vulkan driver performs one copy from VkImage to Vulkan swapchain image (which itself is a CAMetalDrawable), so it is one copy performed by MoltenVk on behalf of us from a IOSurface to a fullscreen native CAMetalDrawable, just like in your case.
  3. Both our PsychVulkanCore driver and the MoltenVK Vulkan-on-top-of-Metal drivers are rather thin layers with essentially no overhead beyond one IOImage → CAMetalDrawable copy in step .
    So most of it is very similar to what we do now through Vulkan. Comparison between the Vulkan+MoltenVK approach and your Metal approach could provide additional clues, by comparison. We’ll see.

One thing we don’t use is CoreAnimation callback driven presents. I tried that many times over the last 20 years and CoreAnimation was always found to be broken or deeply unreliable in different ways on macOS in all versions on PowerPC, Intel, and early Apple Silicon testing. Also high unwanted latency if i remember correctly. So there’s one difference in present scheduling… The other difference is in window creation, I guess, where I found various weird macOS bugs lingering on macOS + Metal.

Wrt. future backends, something I will explore in the future is the recently introduced new KosmickKrisp Vulkan driver for macOS 26+ only on Apple Silicon only. KosmickKrisp is part of the Mesa graphics library that provides the open-source OpenGL, Vulkan, OpenCL and video acceleration drivers for Linux. Mesa is increasingly also ported to Windows + Direct3D 12 with involvement of experts from Microsoft, and now macOS Apple Silicon + Metal. The work is done by LunarG and the community and sponsored at least by Khronos, possibly also by Valve althoug I might misremember. Mesa also has an OpenGL driver layered on top of Vulkan. And a large community of top-notch Linux graphics developers. In fact, KosmickKrisp as a Vulkan driver for macOS was started after the Asahi Linux team - the people trying to port Linux to Apple Silicon - proved that it is possible to implement a fully conformant state of the art OpenGL and Vulkan driver on Apple Silicon. Porting many of the concepts developed for Linux on Apple Silicon now to KosmickKrisp, we might get a fully state of the art, high quality, high performance OpenGL and Vulkan stack on top of Metal… Hans Kristian Arntzen (sponsored by Valve) and myself and a few others have been working the last months of improving Mesa’s Vulkan timing facilities, as a first step on Linux. That will eventually extend to KosmickKrisp.

So there’s future hope for a fully open-source moden OpenGL on all platforms + useful sprinkles of Vulkan where it provides added benefit. And more expert eyes on these issues…

Anyway, beach time.

Mario.

Please tell your girlfriend to confiscate your laptop until your holiday is over.

P



| mariokleiner
August 18 |

  • | - |

Hi Peter, that behaviour is gone as of MacOS 27 (Golden Gate), at least the most recent developer beta. Now the demos all work fine with no frame-rate halving. But the “when” parameter of the Screen(‘Flip’) command is still broken.

Interesting. You mean regular Psychtoolbox demos now work at full framerate?

Yes, and the change happened between PTB 3.0.22.2 and PTB 3.0.22.3. It doesn’t seem to have anything to do with MacOS. I upgraded my laptop to the latest MacOS 27 beta 5 and PTB 3.0.22.3, and the demos worked without any half-rating. On my desktop, I also upgraded to the latest MacOS 27 beta, but I tried out PTB 3.0.22.2 first, and DotDemo still exhibited half-frame-rate for the first run only, and thereafter it worked fine. Incidentally, whenever the PTB purple screen at the beginning stays up for several seconds, that’s an indicator that the frame rate will be half. But now that only happens on the first run.

On vacation I have my small M1 MacBook Air, which resides and travels with my girlfriend. I upgraded it to the latest macOS 26.6.1 Tahoe, tested with Octave (no Matlab license available right now), and found it to be even more broken than the earlier 26.2/3 Tahoe.

I got everything from 1 second stalls, not showing anything on the screen (discarding every single frame), running at full refresh rate while totally ignoring the ‘twhen’ parameter, returning completely made up timestamps, to one (!) run where everything worked once wrt. timing and timestamping, just to break then again. Nothing I tried made it any better.

Then I gave up and remembered I’m on my first real vacation in over a year on a beautiful greek island and swimming, reading, eating and drinking cocktails is much more fun.

<:smiling_face_with_sunglasses:>

So this improved significantly?

Yes, all of the demos work at the full frame rate for PTB 3.0.22.3, no missed frames. Of course VBLSyncTest(,n) still fails if n >= 3, so “working” has some qualifications….

PTB’s Vulkan backend with no twhen does schedule for present at next refresh, and at least in direct-to-display fullscreen mode under the right conditions one should be able to present without 2-3 frames latency. At least some tests with earlier macOS versions did show that.

It also waits for real timestamp availability, and before macOS 26 those timestamps did become available within ~1 msec after present completion. All the most horrible trouble started with macOS 26.

So if regular PTB demos can run at full refresh and provide rather noise-free timestamps (noise in the low single-digit microsecond range), that would be an improvement for macOS 27.

Full refresh yes. As for the timestamps, they are available quickly, but they are noisy, a spread of something like 1.9 ms.

Your implementation could be useful for me as another reference, to see if it behaves different from the Vulkan/MoltenVK implementation. The internal approach is almost identical to what we do now:

  1. Screen’s framebuffer attachment to the imaging pipeline is a IOSurface, which is represented (=acts as backing storage for) by a OpenGL color texture on the Screen side, and as VKImage on the PsychVulkanCore side. No copies are performed, it is zero copy on the interop side.
  2. Internally our Vulkan driver performs one copy from VkImage to Vulkan swapchain image (which itself is a CAMetalDrawable), so it is one copy performed by MoltenVk on behalf of us from a IOSurface to a fullscreen native CAMetalDrawable, just like in your case.
  3. Both our PsychVulkanCore driver and the MoltenVK Vulkan-on-top-of-Metal drivers are rather thin layers with essentially no overhead beyond one IOImage → CAMetalDrawable copy in step .
    So most of it is very similar to what we do now through Vulkan. Comparison between the Vulkan+MoltenVK approach and your Metal approach could provide additional clues, by comparison. We’ll see.

One thing we don’t use is CoreAnimation callback driven presents. I tried that many times over the last 20 years and CoreAnimation was always found to be broken or deeply unreliable in different ways on macOS in all versions on PowerPC, Intel, and early Apple Silicon testing. Also high unwanted latency if i remember correctly. So there’s one difference in present scheduling… The other difference is in window creation, I guess, where I found various weird macOS bugs lingering on macOS + Metal.

CAMetalDisplayLink schedules about 3 refreshes out (2 with Game Mode on) where acquiring a drawable and presenting inline gets to about 1.7, so I made direct presentation the default. It’s reliable but adds a frame of latency over direct presentation.

In my PsychMetal demo, I’ve fixed the “when” parameter and also it returns immediately a highly accurate predicted timestamp, that you can also check later.

The best I’ve been able to do is to have “flip” present not the immediate next frame (N+1) but the frame after that (N+2).

Wrt. future backends, something I will explore in the future is the recently introduced new KosmickKrisp Vulkan driver for macOS 26+ only on Apple Silicon only. KosmickKrisp is part of the Mesa graphics library that provides the open-source OpenGL, Vulkan, OpenCL and video acceleration drivers for Linux. Mesa is increasingly also ported to Windows + Direct3D 12 with involvement of experts from Microsoft, and now macOS Apple Silicon + Metal. The work is done by LunarG and the community and sponsored at least by Khronos, possibly also by Valve althoug I might misremember. Mesa also has an OpenGL driver layered on top of Vulkan. And a large community of top-notch Linux graphics developers. In fact, KosmickKrisp as a Vulkan driver for macOS was started after the Asahi Linux team - the people trying to port Linux to Apple Silicon - proved that it is possible to implement a fully conformant state of the art OpenGL and Vulkan driver on Apple Silicon. Porting many of the concepts developed for Linux on Apple Silicon now to KosmickKrisp, we might get a fully state of the art, high quality, high performance OpenGL and Vulkan stack on top of Metal… Hans Kristian Arntzen (sponsored by Valve) and myself and a few others have been working the last months of improving Mesa’s Vulkan timing facilities, as a first step on Linux. That will eventually extend to KosmickKrisp.

Good to know.

So there’s future hope for a fully open-source moden OpenGL on all platforms + useful sprinkles of Vulkan where it provides added benefit. And more expert eyes on these issues…

I’m by no means an expert, I just was trying this out as a vibe coding exercise, and I learned a bit about the internal workings of Metal and the PTB in the process.

Keith

Yes, and the change happened between PTB 3.0.22.2 and PTB 3.0.22.3. It doesn’t seem to have anything to do with MacOS. I upgraded my laptop to the latest MacOS 27 beta 5 and PTB 3.0.22.3, and the demos worked without any half-rating. On my desktop, I also upgraded to the latest MacOS 27 beta, but I tried out PTB 3.0.22.2 first, and DotDemo still exhibited half-frame-rate for the first run only, and thereafter it worked fine. Incidentally, whenever the PTB purple screen at the beginning stays up for several seconds, that’s an indicator that the frame rate will be half. But now that only happens on the first run.

There hasn’t been any change between 3.0.22.2 and 3.0.22.3 that would explain this. Also no change in build system, XCode, SDK’s, MoltenVK etc. Also the mex files for Matlab are identical between the two versions, only on Octave do they differ a supposedly insignificant bit.

On my up to date macOS 26.6.2 all versions going back to at least December last year are equally broken, on Octave, as I can’t test on Matlab right now. All I observed is things being much worse after upgrading from 26.3 to 26.6.x :confused:

There’s one tiny optimization in 3.0.22.3 for Octave whose existence or absence makes no difference on macOS 26. If it did on macOS 27 that would be another new bug in Metal.

So if regular PTB demos can run at full refresh and provide rather noise-free timestamps (noise in the low single-digit microsecond range), that would be an improvement for macOS 27.

Full refresh yes. As for the timestamps, they are available quickly, but they are noisy, a spread of something like 1.9 ms.

That’s what I see as well, only on latest Tahoe, which means all timestamping is completely broken and nothing is to be trusted timing wise :frowning: .

In my PsychMetal demo, I’ve fixed the “when” parameter and also it returns immediately a highly accurate predicted timestamp, that you can also check later.

Unfortunately prediction is nothing to be trusted in most situations. And collecting proper timestamps after a run is not useful for most paradigms. The Linux PTB has this functionality for very high performance presentation since 15 years (PerceptualVBLSyncTestFlipInfo[2].m), not the Window or macOS versions though.