Frame pacing · Frame generation · Shipped in v20.1

Why 16.67 ms is hard: frame pacing and frame generation on a 120 Hz OLED

Frame generation can put a new image on screen at every refresh. Making each of those images stay up for the same length of time is a separate problem, and it is the one this post is about.

Update: shipped in v20.1

The presenter described here shipped in v20.1, opt-in behind EDEN_NX2_FLUID=1. On the Arc 140T with frame generation on, 601 of 601 slots on the title screen were on time (100% of shown frames within 2 ms of the median), and 75% of shown frames in Hyrule Field were within 2 ms, against 33-54% before. That is one device, so it stays opt-in until another device agrees. The diagnosis and design below are as written before the result.

What smooth means

A display refreshes on a fixed beat. At 60 Hz that beat is 16.67 ms. The panel on our test laptop is a 120 Hz OLED, so it refreshes twice in every 16.67 ms slot. Pacing is about evenness: if one frame stays up for one slot and the next for two, motion visibly hitches, even when the average frame rate looks fine. A panel that switches pixels as fast as an OLED does nothing to blur that away.

Frame generation fills the slots the game did not render. In Guarana it predicts instead of interpolating: each generated frame moves the newest real frame's pixels forward along the game's own motion vectors, using depth to decide which surface is in front. Because it never waits for a future frame, it adds no latency. The HUD is kept apart, so text and menus are not dragged along with the scenery.

Where v20 stands

In the frame-generation play config, the previous build was locked to 50 ms frames, 19.6 game fps. v20 reads about 26.7 game fps live. That is a live reading, not a same-spot A/B, so treat it as a sign rather than a measurement. Game fps counts only the frames the game renders.

About 26.7 real frames a second does not divide 60 Hz evenly. Some real frames have to cover more 16.67 ms slots than others, and when the presenter cannot keep every shown frame up for the same time, you see micro-stutter. v20 lists it as a known issue.

The diagnosis: three things working against even holds

We followed a finished frame from the renderer to the screen and found three causes.

1. FIFO, forced

With frame generation on, the swapchain's present mode is forced to FIFO, or FIFO relaxed. Every present waits in a queue and becomes visible at a vblank. That is tear-free and correct, but it means the screen only changes on refresh boundaries, whatever time the emulator intended. On our test laptop the driver offers only the immediate and FIFO modes for the window, and no present-wait extension that would report when a frame actually reached the screen.

2. A present thread that is not locked to vblank

Each generated frame carries a spacing: an equal share of the measured real frame interval. The present thread holds a frame for its spacing, then presents the next one. Those waits run on the CPU clock, not the display's. So the moment a frame enters the FIFO queue drifts against the refresh grid: one frame catches the next vblank, the next one just misses it and waits for the one after. Frames meant to stay up for equal times end up held for different numbers of refreshes.

3. Fixed-fraction prediction

Each predicted frame is extrapolated to a fixed fraction of the real frame interval, chosen before anyone knows when that frame will be shown. If it then reaches the screen a refresh early or late, it shows motion from the wrong moment, and things jump ahead or lag behind. The prediction and the display disagree about what time it is.

Each cause is small on its own. Together they turn generated frames into uneven motion.

Shown frames against 16.67 ms slots, today and with a vblank-locked presenter In the top row, frames handed over on the CPU clock are held for one or two slots unevenly. In the bottom row, every shown frame lasts exactly one slot. 16.67 ms slots Today: spacing on the CPU clock, then FIFO Some frames stay up for one slot, some for two: micro-stutter. Vblank-locked presenter (v20.1, opt-in) Every shown frame, real or predicted, stays up for one slot. real frame predicted frame
Illustration, not a capture. On a 120 Hz panel each 16.67 ms slot covers two refreshes.

The presenter

The fix is a presenter locked to the display's vblank. It is experimental and ships in v20.1 behind EDEN_NX2_FLUID=1:

  • A vblank clock. The present thread waits for the display's own vblank instead of sleeping for a computed spacing. On Windows it waits on the window's display output, with fallbacks when that is not available, and it follows the display when its refresh rate changes.
  • Slots, not sleeps. Frames are scheduled into 16.67 ms slots, and each frame carries the slot it is meant for. On a 120 Hz panel a slot covers two refreshes.
  • The newest finished frame wins. At each slot the presenter shows the newest frame whose GPU work is done. If none is ready, the slot counts as a miss. A frame whose slot has passed is dropped unshown rather than shown late.
  • Predictions timed to their slot. A predicted frame is extrapolated to the moment its slot reaches the screen, not to a fixed fraction of the interval.
  • One present ahead, at most. Only one present waits ahead of the display, so a late frame cannot push everything behind it.
  • Phase lock. While the game keeps a steady pace, real frames are nudged to arrive just before their slot instead of drifting against the display.

What done looks like

The roadmap item for this work, N1, is done when:

  • a present-level capture in Hyrule Field with frame generation on shows every shown frame lasting one 16.67 ms slot;
  • game fps in the same scene is not lower than v20, in valid benchmark rows;
  • it is on by default and the switch is gone, once rows from the Arc 140T and at least one other device agree.

None of that is done yet. When the captures exist, they will be published with their method, the same way the shader fix numbers were.

Help wanted

  • Shown-frame captures on 60 Hz and high-refresh panels: PresentMon's display-change intervals next to the emulator's own numbers, from the same run, with the refresh rate and whether variable refresh was on.
  • Issue #8: present-mode logging, shown-frame telemetry, and an A/B suite for hardware-accelerated GPU scheduling on and off and for immediate against FIFO. It is labelled agent-ready, and its tests run without the game.
  • Device reports from any iGPU laptop or handheld, through the device report form.

Questions people ask

Does frame generation make the game respond faster?

No. It makes motion look smoother. The game still runs and reads input at its own frame rate. The FAQ has more.

Why predict instead of interpolate?

Interpolation needs the next real frame before it can show the frame in between, so it has to hold real frames back. Prediction shows each real frame as soon as it is ready and fills the time after it. Camera cuts and frames without motion data are shown alone.

Can I try the new presenter in v20?

Not yet. It will arrive as an experiment behind EDEN_NX2_FLUID=1. Follow issue #8 and the changelog; when it lands, test reports with your display's refresh rate are exactly what we need.

Help make pacing even on your screen

Every device except the Arc 140T is untested by the maintainers. Send a device report, capture shown frames, or pick an agent-ready issue. How to contribute.