Metal drawable sync rate : try 2

Further in my adventures to correctly control the sync rate of frames in Metal. Building on previous post:


https://forums.developer.apple.com/thread/16293


We were recommended to use [MTLCommandBuffer presentDrawable: atTime:]


Best we can tell, this doesn't reliably work.


I've modified the MetalBasic3D example to use this technique. The changes are:

-change CADisplayLink frameInterval 1 -> 2 (i.e. 30fps).

-change the code in AAPLRenderer to use presentAtTime:


The present time is calculated as:

float frameTime = 1.f/30.f;

double nextPresent = CADisplayLink.timeStamp + frameTime * 1.6f;


i.e. we want to use the second 30fps present after the last (assuming building and consumption of the command buffer will be greater than 33ms - a reasonable expectation in our titles).


Results:

iPhone6 & iOS8 = Black screen. Occasional flashes of correct frame.

iPadAir2 & iOS8.1.3 = Black screen. Occasional flashes of correct frame.


iPhone6+ & iOS9 = Mostly correct. Ocassional flashes of black frames.


Comments? Is there a Metal example demonstrating this technique working reliably?


cheers - j

Hi jrb,


It looks like your target present time is too far in the future. You want to ensure your present doesn't land in the current 60Hz interval but the next one so should target between 17ms and 33ms into the future (1.f/60.f * 1.2f would be a good choice).


iOS 8 did have a bug that prevents presentAtTime: from working though. We're looking for a workaround now.


Cheers

Hi PJ -


Thanks for the feedback, but unfortunately that can't work in our case.


While CA has a native 60hz refresh - we're targetting 30hz via CADisplayLink.


>>You want to ensure your present doesn't land in the current 60Hz interval but the next one so should target between

>>17ms and 33ms into the future (1.f/60.f * 1.2f would be a good choice).


That math assumes the building of our command buffer + driver & GPU consumption all happen within 2x 60hz ticks.


We've got two operations to do -- build the command buffer, and have the driver + GPU consume the command buffer.

The odd of that happening completely within the current 33ms frame (from CADisplayLink tick -> 2x 60hz intervals) is zero 🙂 Thats why we target 30hz not 60hz. And we're assuming the driver + GPU run in parallel with our CPU thread building the next command buffer.


(I'd attach a diagram from Metal System Trace, but it seems forum posts + images places them in moderation limbo, i'll attempt with ASCII)


What we're expecting to do:


a) Time 0 (CADisplayLink tick).

Build CommandBuffer0 + schedulePresent0.

Commit CB0. (Driver & GPU might start consumption)


b) Time 33ms (CAD tick + 1).

Build CB1 + schedulePresent1.

Driver & GPU are *still* consuming CB0 in parallel - when done, waits for c) to present


c) Time 66ms (CAD tick + 2).

CB0 is presented.

Build CB2 + schedulePresent2.

Driver & GPU consuming CB1 in parallel.


d: Time 99ms (CAD tick + 3)

CB1 is presented


...etc. The pattern continues.


So at a), we need to schedule the present to happen at c), which is (33ms * 1.6) -Aiming for 60hz * 4 vsync.


This all works as expected when we use the basic presentDrawable: -- all the expected parallelism between command buffers and the GPU is fine 🙂. Our workload is generally heavy and consistent enough we present @ 2 x 30hz (i.e. 4 x 60hz) from the display tick. The problem we're trying to solve is the occasionally fast frame that finishes on the GPU before 3 x 60hz tick, which leads to inconsistent output pacing.


I guess something I could try is change the commit & schedule present of the CB from when the building is done to the start of the *next* frame (i.e. change schedulePresent0 + commit CBO0 from a) -> start of b)).


Scheme 2


a) Time 0 (CADisplayLink tick).

Build CB0


b) Time 33ms (CAD tick + 1).

scedulePresent0 + commit CB0

Driver & GPU consuming CB0 in parallel - when done, waits for c) to present

build CB1


c) Time 66ms (CAD tick + 2).

CB0 is presented

schedulePresent1 + commit CB1

Driver & GPU consuming CB1 in parallel.

CB0 is presented.


Then the schedule present *would* follow your path. BUT we're now potentially leaving the GPU idle until the next CA tick. Not ideal.


Cheers! - j

Ah, thanks for clarifying. I can reproduce the occassional black frame you're describing in a sample app. A workaround would be to create a serial dispatch queue to present your frames at the target time:


// Game state:

dispatch_queue_t _presentQueue;

dispatch_time_t _currentTime;


// At game init:

_presentQueue = dispatch_queue_create("PresentQueue", DISPATCH_QUEUE_SERIAL);


// Per-frame start, capture current time:

_currentTime = dispatch_time(DISPATCH_TIME_NOW, 0);


// Per-frame end, schedule the present:

[commandBuffer addScheduledHandler:^(id<MTLCommandBuffer>) {

dispatch_after(dispatch_time(_currentTime, 1.0/30.0 * 1.6 * NSEC_PER_SEC), _presentQueue, ^{

[drawable present];

});

}];


Let me know how it goes!

Metal drawable sync rate : try 2
 
 
Q