Compare commits

...
Author SHA1 Message Date
LibretroAdmin bb4be745e5 scanline sync: hand the vblank clock to the frame thread by seqlock, not a lock
The vblank clock was shared under an slock: the kernel-event thread
took it once per vblank to feed the clock, and the frame thread took it
two or three times a frame to read the beam. Uncontended that costs
little, but it lets the frame thread wait behind the time-critical
clock thread, and the house pattern for a one-writer snapshot read by
another thread is a seqlock - video_driver.c's frame cache, the
viewport, the shader sensor uniforms.

The clock has exactly one writer at a time: the kernel-event thread
while it runs, the frame thread otherwise (the DWM and DirectDraw
sources, and every reset). Every source switch stops the thread before
the frame thread writes and starts it after, and thread start and join
order that handover, so the writer's clock is plain memory. What the
frame thread reads is a published copy:

- vblank_clock_publish() / vblank_clock_snapshot(), in vblank_clock.h:
  a stamp made odd around four relaxed 64-bit and int stores - the
  period and nominal rate as the bits of their doubles - and a reader
  that retries against a publish in progress up to 64 times, then
  takes "no clock this once". The same shape and bound as the frame
  cache's. The writer never waits and the reader never blocks.

- Available where retro_atomic is lock-free and 64 bits wide, which
  both MinGW targets are - on 32-bit x86 the 64-bit fields ride
  cmpxchg8b, which every processor Windows 2000 runs on has. A build
  without them keeps the lock, as the frame cache does.

The DirectDraw source, whose writer is the frame thread itself, reads
its own copy instead of the snapshot.

samples/gfx/vblank_clock/vblank_clock_pub_test: one writer publishing
two million clocks flat out, each built so its fields agree with each
other, and three readers checking every snapshot is one whole publish
and that none goes backwards. Five to six million snapshots, none torn
or backwards; clean under ThreadSanitizer, ASan and UBSan, and run
under TSan in the samples/gfx job. (Under TSan's slowdown about 5% of
reads hit the retry bound against a writer that never pauses; the real
one publishes once a vblank.)
2026-09-18 18:20:53 +02:00
LibretroAdmin 2fbd665566 scanline sync: feed the vblank clock from DWM, the kernel event, or DirectDraw readings
The vblank clock had one source, a thread waiting on
D3DKMTWaitForVerticalBlankEvent, which exists only on WDDM. Scanline
Sync now picks its source per display, best first, and all three feed
the same clock; the frame side - beam by arithmetic, one timer to the
target line - is unchanged.

Read from the binaries first. DXGI's WaitForVBlank resolves
D3DKMTWaitForVerticalBlankEvent by name (dxgi 10.0.26100), and
DirectDraw on WDDM builds WaitForVerticalBlank as a user-mode loop over
D3DKMTGetScanLine - no sleep, no bound, under DirectDraw's process-wide
lock (ddraw 6.1.7600, DdWaitForVerticalBlank, the loop at 71065e12). So
on Vista and later neither is a way in that is better than calling the
export, and the kernel-event tier keeps calling it directly. On XP
(ddraw 5.3.2600.5512, symbols through the OMAP of its BBT layout),
WaitForVerticalBlank makes one call into the display driver and blocks
or spins as that driver sees fit, holding the same lock, and its event
mode returns E_NOTIMPL; GetScanLine is one call into the driver.

- DWM (Vista and later with composition on; always from Windows 8):
  DwmGetCompositionTimingInfo's last vblank and refresh period, read on
  the frame thread - no thread, no kernel wait. With composition on a
  frame reaches the screen at DWM's next composition, which runs on
  this clock. Used only while DWM's period matches the window's
  monitor, since DWM paces to one display; and only while its
  timestamps advance, since exclusive fullscreen stops them. It takes
  over from the kernel event after 60 advancing frames, so a window
  flicking in and out of exclusive fullscreen does not keep restarting
  the clock.

- The kernel vblank event (WDDM): the existing thread, now one source
  among three.

- DirectDraw readings (XDDM - Windows 2000 and XP, where no D3DKMT
  exists): one IDirectDraw::GetScanLine per call on the frame thread,
  on the DirectDraw object DirectDrawEnumerateEx gives for the
  window's monitor. WaitForVerticalBlank is not used.
  vblank_sampler_feed(), new beside the clock, turns each reading into
  the time of the vblank it implies, measuring the beam's line rate
  from pairs of readings in the same frame and the total line count
  from the rate and the period, which XDDM has no API to report. The
  mode's whole-Hz rate from EnumDisplaySettings is only a start - a
  driver reporting "hardware default" starts from 60 Hz - and the clock
  learns the true period. A driver without GetScanLine stops being
  asked after 60 failures.

Until the clock has settled the frame is told there is no scanline, so
Scanline Sync idles instead of polling the counter while it settles.
Only a display none of the three serves - Vista, with no
QueryDisplayConfig for the line counts - still reads the counter.
ddraw.dll and dwmapi.dll are loaded at run time, so nothing is linked
that Windows 2000 lacks; a 32-bit build for _WIN32_WINNT 0x0500
compiles.

samples/gfx/vblank_clock gains the sampler: a counter read twice a
frame at varying points, each read taking 2-30 us, blanking reported
as an error, a 59.94 Hz display started from 60. The total line count
comes out exact and the beam within 2 lines. samples/gfx/vblank_clock_ddraw
runs the tier against a real DirectDraw - wine's, which reports the
counter as an XDDM driver does - and checks the clock's beam against
GetScanLine: within 2-5 lines over 115-119 checks. It runs under wine
in the samples/gfx job.

Not run on real hardware: the DWM and kernel-event tiers need Windows,
and the XDDM tier's cost per GetScanLine and each driver's report of
blanking are unmeasured on XP itself.
2026-09-18 18:03:46 +02:00
LibretroAdmin 9dd810c96e scanline sync: race the beam on a vblank clock and a timer, not by polling the counter
Scanline Sync waited for its target line by calling D3DKMTGetScanLine
in a loop, after a sleep that did not look at the beam at all. Read
from dxgkrnl.sys (10.0.26100.7462, its own PDB), that call is a system
call that probes its input, looks the adapter up by handle, takes the
adapter's DDI lock, checks IRQL and dispatches into the display
miniport's DxgkDdiGetScanLine - about 223 us a call on the panel the
tree's comment measured, some 15 lines at 1080p60. And the tuner
measured those lines against RetroArch's viewport height, which is the
display's only fullscreen at the desktop resolution.

D3DKMTWaitForVerticalBlankEvent, measured in the same comment within
30 us of the real vblank with its period accurate to 0.02%, was already
resolved and never called. In dxgkrnl the wait is a KeWaitForSingleObject
on the adapter's vblank event with a fixed 80 ms timeout, so it cannot
hang.

- A vblank clock thread waits on that event for the window's monitor,
  at time-critical priority, and feeds each timestamp to
  gfx/common/vblank_clock.h. A failed wait is never taken for a vblank;
  an error that returns at once backs off on the thread's stop event.
  Stopping it joins a wait the kernel bounds.

- The mode's active and total lines, and its exact rate, come from
  QueryDisplayConfig, matched to the counter's output by the adapter
  LUID and VidPn source ID that D3DKMTOpenAdapterFromHdc reports
  (win32_display_signal_timing, in dispserv_win32.c beside the
  existing QueryDisplayConfig code). The tuner measures in those lines
  now, and takes the total outright instead of watching a frame to
  count it. Interlaced modes are left out.

- vblank_clock.h is the arithmetic, pure and on its own. A waiter only
  ever wakes late, so it tracks the lower edge of the timestamps: an
  early arrival pulls the phase by half, a late one by a sixty-fourth,
  and the period is corrected from the same error. A whole number of
  periods is missed vblanks and keeps the settle count; anything else
  starts over; the same vblank reported twice is ignored. The beam is
  reported only after 120 good vblanks, and never once none has come
  for two periods - and a period that wanders, variable refresh, never
  settles, so it is never used there.

- D3DKMTGetScanLine answers from the clock when it is running, and
  reads the counter only when it is not. After a frame, the wait for
  the target line is one high-resolution waitable timer aimed at the
  moment the clock says the beam gets there, with a rebind event - the
  window moved, the mode changed - ending it at once, and a bound. No
  polling, no sleep. The counter loop is kept, and bounded, for when
  there is no clock.

samples/gfx/vblank_clock drives vblank_clock.h against 50 simulated
displays up to 600 ppm off their nominal rate, with a waiter waking up
to 460 us late. Beam and wait landing stay within 4 lines (6 is
asserted); the period within 1.8 us. Getting there took the test: a
first version, smoothing the timestamps as if lateness were symmetric,
was 16 lines out, and reporting after 8 vblanks let it be 54 lines out
while converging. Missed and duplicate vblanks, a stale clock, variable
refresh and a target already passed are covered. It runs under C89,
ASan and UBSan in the samples/gfx job.

Not run on hardware: wine has no D3DKMTGetScanLine, and nothing here
has a display. The Windows side is syntax-checked with MinGW and the
linux-c89 flags.
2026-09-18 18:03:42 +02:00
LibretroAdmin 8d61347d46 scanline sync: bound the beam wait, and read the monitor the window is on
Two faults in Scanline Sync, the D3DKMT beam-racing path on Windows.

The wait after a frame spun on D3DKMTGetScanLine until the beam reached
the target line, and that was its only way out. The before-frame tuner
checks once per frame that the query works, but a query that starts
failing during the wait - the display going to sleep, a mode change,
the lock screen, an adapter reset - returns -1, which never reaches the
target: the frame thread spun at full CPU for good. The wait now leaves
at once on a failed read, keeping the tuner's state as it was, and is
bounded to two frames, which covers reaching the target and the
calibration pass that measures a whole frame.

And the scanline counter was opened on whichever display device
answered first at startup - usually the primary - whatever monitor the
window was on, so on a second screen the sync raced another display's
beam, possibly at another refresh rate. It is now opened on the adapter
output driving the window's monitor. WM_MOVE and WM_DISPLAYCHANGE flag
a possible change, and the thread that reads the counter rebinds it
before its next read, so the adapter state is only touched by that
thread; with nothing changed that costs one atomic load. The adapter
handle being replaced is closed with D3DKMTCloseAdapter where gdi32
has it; they were never closed before.

Syntax-checked with MinGW and the linux-c89 flags. Not run: wine has no
D3DKMTGetScanLine, and nothing here has a display to race.
2026-09-18 18:03:38 +02:00
11 changed files with 1833 additions and 70 deletions

No files matched your search

+45
View File
@@ -180,6 +180,51 @@ jobs:
done
echo "[pass] atomic backend portability"
- name: Build and run vblank_clock_ddraw_test (MinGW, under wine)
shell: bash
working-directory: samples/gfx/vblank_clock_ddraw
run: |
set -eu
# Scanline Sync's Windows 2000/XP tier against a real DirectDraw
# (wine's, which reports the counter as an XDDM driver does):
# GetScanLine readings into vblank_sampler_feed and the vblank
# clock, and the clock's beam checked against GetScanLine.
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 \
|| ! command -v wine >/dev/null 2>&1 \
|| ! command -v xvfb-run >/dev/null 2>&1; then
sudo apt-get install -y -qq gcc-mingw-w64-x86-64 wine64 xvfb \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30
fi
make clean all
WINEDEBUG=-all timeout 300 xvfb-run -a -s "-screen 0 1280x1024x24" make check
echo "[pass] vblank_clock_ddraw_test (wine)"
- name: Build and run vblank_clock_test and vblank_clock_pub_test (C89, ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/vblank_clock
run: |
set -eu
# gfx/common/vblank_clock.h: the arithmetic Scanline Sync on
# Windows uses in place of polling D3DKMTGetScanLine - a clock
# fed vblank timestamps from a waiter that only ever wakes
# late, and the beam position it implies. Against 50 simulated
# displays up to 600 ppm off their nominal rate: no beam
# before it settles, beam and wait landing within 6 lines,
# missed and duplicate vblanks, a stale clock, and variable
# refresh never reported.
make clean all SANITIZER=address,undefined
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 60 ./vblank_clock_test
echo "[pass] vblank_clock_test"
# The seqlock handing the clock from the kernel-event thread to
# the frame thread: one writer publishing flat out, three
# readers checking every snapshot is one whole publish and
# none goes backwards - under ASan+UBSan, then TSan, which
# also checks the pattern is free of formal data races.
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 ./vblank_clock_pub_test
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./vblank_clock_pub_test
echo "[pass] vblank_clock_pub_test (ASan, TSan)"
- name: Build and run vulkan_extension_count_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_extension_count
+313
View File
@@ -0,0 +1,313 @@
/* RetroArch - A frontend for libretro.
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
/* A clock driven by vblank timestamps, and the beam position it
* implies. Pure arithmetic, no platform calls: the caller feeds it the
* time of each vblank it observes and asks it where the beam is at a
* given moment. Scanline Sync uses it on Windows, fed from
* D3DKMTWaitForVerticalBlankEvent, in place of polling
* D3DKMTGetScanLine; samples/gfx/vblank_clock tests it on its own.
*
* A waiter only ever wakes late, so the clock tracks the lower edge of
* the timestamps: it follows an early arrival strongly and a late one
* barely, and corrects the period from the same phase error. A gap that is a whole number of periods is
* missed vblanks and only moves the phase; anything else starts over
* from the nominal rate. A period that will not settle - variable
* refresh - never reaches VBLANK_CLOCK_SETTLE good vblanks in a row,
* so the clock is never used for it. */
#ifndef __VBLANK_CLOCK_H
#define __VBLANK_CLOCK_H
#include <math.h>
#include <stdint.h>
#include <string.h>
#include <boolean.h>
#include <retro_inline.h>
#include <retro_atomic.h>
#include <retro_miscellaneous.h> /* retro_cpu_relax */
/* Vblanks that must fit the period before the beam is reported: how
* long the phase and period take to converge from the mode's nominal
* rate. Measured against simulated displays up to 600 ppm off nominal
* with a waiter waking up to 460 us late: 8 let the beam be reported
* 54 lines out while still converging, 120 keeps it within 5
* (samples/gfx/vblank_clock). Two seconds at 60 Hz. */
#define VBLANK_CLOCK_SETTLE 120
typedef struct vblank_clock
{
int64_t last_us; /* phase: the time of the last vblank */
double period_us; /* smoothed */
double nominal_us; /* the mode's rate, to start from */
unsigned good; /* consecutive vblanks that fit the period */
} vblank_clock_t;
static INLINE void vblank_clock_init(vblank_clock_t *c, double nominal_us)
{
memset(c, 0, sizeof(*c));
c->nominal_us = nominal_us;
}
/* A vblank wait that did not end in a vblank (a timeout, an error):
* the run of good vblanks is broken. */
static INLINE void vblank_clock_miss(vblank_clock_t *c)
{
c->good = 0;
}
/* A vblank observed at @now_us. */
static INLINE void vblank_clock_feed(vblank_clock_t *c, int64_t now_us)
{
double gap, k, off;
if (!c->last_us || c->period_us <= 0.0)
{
c->period_us = c->nominal_us;
c->good = 0;
c->last_us = now_us;
return;
}
gap = (double)(now_us - c->last_us);
k = floor(gap / c->period_us + 0.5);
off = gap - k * c->period_us;
/* Less than half a period after the last one: the same vblank
* reported twice, not a new one. */
if (k < 1.0)
return;
if (fabs(off) < 0.1 * c->period_us)
{
/* A waiter wakes late, never early, so the timestamps are the
* true vblanks plus a one-sided lateness, and the estimate to
* track is their lower edge. An arrival earlier than predicted
* says the prediction runs late: follow it by half. One later
* than predicted is mostly the waiter's own lateness: follow it
* by a sixty-fourth, which is what lets a period that is really
* longer creep in. The period is corrected from the same phase
* error, spread over the k periods it built up over. */
double predicted = (double)c->last_us + k * c->period_us;
double corr = (off < 0.0) ? off / 2.0 : off / 64.0;
c->last_us = (int64_t)(predicted + corr);
c->period_us += corr / (k * 8.0);
/* Missed vblanks - a whole number of periods - still fit: they
* keep the count, only a clean one adds to it. Resetting on them
* left the clock usable 8.5% of the time with 2% missed. */
if (k == 1.0)
c->good++;
}
else
{
c->period_us = c->nominal_us;
c->good = 0;
c->last_us = now_us;
}
}
/* The beam line at @now_us for a mode of @active of @total lines,
* counting from the first blanking line, where the vblank event fires;
* or -1 while the clock has not settled, or when it has seen no vblank
* for two periods (display asleep, a mode change in flight). */
static INLINE int vblank_clock_beam(const vblank_clock_t *c, int64_t now_us,
unsigned active, unsigned total)
{
double since;
int line;
if ( c->good < VBLANK_CLOCK_SETTLE
|| c->period_us <= 0.0
|| !c->last_us
|| total <= active)
return -1;
since = (double)(now_us - c->last_us);
if (since < 0.0 || since > 2.0 * c->period_us)
return -1;
line = (int)active
+ (int)(fmod(since, c->period_us) / c->period_us * (double)total);
if (line >= (int)total)
line -= (int)total;
return line;
}
/* Microseconds from @now_us until the beam reaches @target_line, or -1
* when the clock cannot say. 0 when the beam is at or past it in this
* frame, as a counter polled until it reached the line would find. */
static INLINE int64_t vblank_clock_until_line(const vblank_clock_t *c,
int64_t now_us, unsigned active, unsigned total, int target_line)
{
int beam = vblank_clock_beam(c, now_us, active, total);
if (beam < 0)
return -1;
if (beam >= target_line)
return 0;
return (int64_t)((double)(target_line - beam) * c->period_us
/ (double)total);
}
/* Vblank timestamps from scanline readings, for a display that has a
* cheap scanline counter and no usable vblank wait - XDDM, Windows 2000
* and XP, where IDirectDraw::GetScanLine is one call to the display
* driver but WaitForVerticalBlank blocks or spins as each driver sees
* fit while holding DirectDraw's process-wide lock.
*
* Each reading in the active region says how long ago the last vblank
* began: the blanking lines, then the lines read, at the beam's line
* rate. The rate is measured from pairs of readings in the same frame at
* least a millisecond apart, and the total line count follows from the
* rate and the period - XDDM has no QueryDisplayConfig to ask. A
* reading taken during blanking (the driver's
* DDERR_VERTICALBLANKINPROGRESS) says nothing about the line and is
* passed as -1. */
typedef struct vblank_sampler
{
int64_t prev_us; /* the last reading in the active region, or 0 */
int prev_line;
double rate; /* lines per microsecond, smoothed */
unsigned rate_n; /* readings the rate is from */
} vblank_sampler_t;
/* Readings the rate must be from before a vblank is reported. */
#define VBLANK_SAMPLER_SETTLE 16
/* A reading of @line, taken at @t_us (the middle of the call). Returns
* the time of the vblank it implies, or 0 while the rate is not known
* yet or the reading says nothing; *total_out gets the implied total
* line count. @period_us is the refresh period as best known. */
static INLINE int64_t vblank_sampler_feed(vblank_sampler_t *s, int64_t t_us,
int line, unsigned active, double period_us, unsigned *total_out)
{
double total;
if (line < 0 || line >= (int)active || period_us <= 0.0)
{
s->prev_us = 0;
return 0;
}
if (s->prev_us && line > s->prev_line)
{
double dt = (double)(t_us - s->prev_us);
/* Same frame (no wrap fits in under 0.8 of a period with the line
* higher than before), and far enough apart to mean something. */
if (dt >= 1000.0 && dt < 0.8 * period_us)
{
double r = (double)(line - s->prev_line) / dt;
if (!s->rate_n)
s->rate = r;
else
s->rate += (r - s->rate) / 16.0;
s->rate_n++;
}
}
s->prev_us = t_us;
s->prev_line = line;
if (s->rate_n < VBLANK_SAMPLER_SETTLE || s->rate <= 0.0)
return 0;
total = floor(s->rate * period_us + 0.5);
if (total <= (double)active)
return 0;
*total_out = (unsigned)total;
/* Since the vblank began: the blanking lines, then @line. */
return t_us - (int64_t)(((total - (double)active) + (double)line)
/ s->rate);
}
/* Publishing a clock from its one writer thread to a reader on another,
* as a seqlock: the writer never waits, and a reader retries a bounded
* number of times against a publish in progress. Available where the
* atomics are lock-free and 64 bits wide; elsewhere a caller keeps a
* lock. The writer's own vblank_clock_t stays plain memory - this is
* only the copy the reader sees. */
#if defined(RETRO_ATOMIC_LOCK_FREE) && defined(RETRO_ATOMIC_HAS_64)
#define VBLANK_CLOCK_HAS_PUB 1
typedef struct vblank_clock_pub
{
retro_atomic_size_t seq;
retro_atomic_64_t last_us;
retro_atomic_64_t period; /* double, as its bits */
retro_atomic_64_t nominal; /* double, as its bits */
retro_atomic_int_t good;
} vblank_clock_pub_t;
/* A publish is four relaxed stores, so a reader that lands mid-write is
* one relax away from a clean sample; the bound keeps it from spinning
* behind a writer descheduled mid-publish. */
#define VBLANK_CLOCK_PUB_TRIES 64
static INLINE int64_t vblank_clock_dbits_(double d)
{
int64_t i;
memcpy(&i, &d, sizeof(i));
return i;
}
static INLINE double vblank_clock_bitsd_(int64_t i)
{
double d;
memcpy(&d, &i, sizeof(d));
return d;
}
/* One writer only. */
static INLINE void vblank_clock_publish(vblank_clock_pub_t *p,
const vblank_clock_t *c)
{
size_t seq = retro_atomic_load_relaxed_size(&p->seq);
retro_atomic_store_release_size(&p->seq, seq + 1);
/* Keeps the field stores below from being hoisted above the odd
* stamp, which is what tells a reader the snapshot is in flux. */
retro_atomic_thread_fence_release();
retro_atomic_store_relaxed_64(&p->last_us, c->last_us);
retro_atomic_store_relaxed_64(&p->period, vblank_clock_dbits_(c->period_us));
retro_atomic_store_relaxed_64(&p->nominal, vblank_clock_dbits_(c->nominal_us));
retro_atomic_store_relaxed_int(&p->good, (int)c->good);
retro_atomic_store_release_size(&p->seq, seq + 2);
}
/* Any thread. false when every try landed on a publish in progress: the
* caller has no clock this once. */
static INLINE bool vblank_clock_snapshot(vblank_clock_pub_t *p,
vblank_clock_t *out)
{
unsigned tries;
for (tries = 0; tries < VBLANK_CLOCK_PUB_TRIES; tries++)
{
size_t s2;
size_t s1 = retro_atomic_load_acquire_size(&p->seq);
if (s1 & 1)
{
retro_cpu_relax();
continue;
}
out->last_us = retro_atomic_load_relaxed_64(&p->last_us);
out->period_us = vblank_clock_bitsd_(retro_atomic_load_relaxed_64(&p->period));
out->nominal_us = vblank_clock_bitsd_(retro_atomic_load_relaxed_64(&p->nominal));
out->good = (unsigned)retro_atomic_load_relaxed_int(&p->good);
retro_atomic_thread_fence_acquire();
s2 = retro_atomic_load_acquire_size(&p->seq);
if (s1 == s2)
return true;
}
return false;
}
#endif
#endif
+753 -65
View File
@@ -119,6 +119,10 @@ typedef char win32_dwm_timing_info_size_check[
#endif
#include "win32_common.h"
#include "vblank_clock.h"
#ifdef HAVE_D3DKMT
#include <ddraw.h>
#endif
#ifdef HAVE_GDI
@@ -247,90 +251,673 @@ typedef REASON_CONTEXT POWER_REQUEST_CONTEXT, *PPOWER_REQUEST_CONTEXT, *LPPOWER_
#ifdef HAVE_D3DKMT
static d3dkmt_adapter_t d3dkmt_adapter;
/* The monitor the scanline counter was opened on. */
static HMONITOR d3dkmt_monitor;
/* Set by the window procedure when the window may have changed
* monitor (moved, or the display layout changed); the thread that
* reads the counter rebinds it before its next read, so the adapter
* state is only ever touched by that thread. */
static retro_atomic_int_t d3dkmt_rebind_pending =
RETRO_ATOMIC_INT_INITIALIZER(0);
/* Signalled with a rebind request, so a frame waiting for a scanline on
* the old monitor's clock stops at once. Auto-reset. */
static HANDLE d3dkmt_wake_event;
typedef NTSTATUS (CALLBACK *D3DKMTCLOSEADAPTER_FN)(const D3DKMT_CLOSEADAPTER*);
static D3DKMTCLOSEADAPTER_FN pD3DKMTCloseAdapter;
/* ---------------------------------------------------------------------
* The vblank clock, and what feeds it.
*
* Scanline Sync races the beam on arithmetic: gfx/common/vblank_clock.h
* keeps the phase and period of the display's vblanks, and the beam at
* any moment follows from the time since the last one. The frame thread
* never polls a counter. Three sources can feed the clock, picked per
* display, best first:
*
* DWM (Vista and later, composition on - always from Windows 8).
* DwmGetCompositionTimingInfo reports the last vblank and the refresh
* period DWM already tracks; the frame thread reads it once per call,
* no thread, no kernel wait. With composition on, a frame reaches the
* screen at DWM's next composition, so this is the clock that
* deadline runs on. Used only while its period matches the window's
* monitor: DWM paces to one display, which on a mixed-rate
* multi-monitor desktop need not be this one. Exclusive fullscreen
* stops its timestamps advancing, and the kernel event takes over.
*
* The kernel vblank event (WDDM). A thread waits on
* D3DKMTWaitForVerticalBlankEvent - read from dxgkrnl, a
* KeWaitForSingleObject on the adapter's vblank event with an 80 ms
* timeout, so it cannot hang - and timestamps each one. Called
* directly: DXGI's WaitForVBlank resolves this same export by name,
* and DirectDraw's WaitForVerticalBlank on WDDM is a busy loop over
* D3DKMTGetScanLine (ddraw 6.1, DdWaitForVerticalBlank), so neither
* is a better way in.
*
* DirectDraw scanline readings (XDDM: Windows 2000 and XP, where no
* D3DKMT exists). IDirectDraw::GetScanLine there is one call into the
* display driver; WaitForVerticalBlank is not used - its only
* blocking modes block or spin in the driver as each driver sees fit,
* holding DirectDraw's process-wide lock, and its event mode returns
* E_NOTIMPL (ddraw 5.3.2600.5512). A reading per call becomes a
* vblank timestamp through vblank_sampler_feed(), which measures the
* line rate and the total line count XDDM has no API to report.
*
* Until the clock has settled - VBLANK_CLOCK_SETTLE good vblanks - the
* frame is told there is no scanline, and Scanline Sync idles rather
* than polling. Only a display none of these can serve (Vista, which
* has no QueryDisplayConfig for the line counts) reads the counter.
* ------------------------------------------------------------------- */
/* One writer at a time: the kernel-event thread while it runs, the
* frame thread otherwise (DWM, DirectDraw, and every reset). Every
* switch stops the thread before the frame thread writes and starts it
* after, and thread start and join order that handover, so the writer's
* copy is plain memory. The frame thread reads a published snapshot,
* through a seqlock where the atomics allow - the writer never waits,
* and the frame thread never waits behind the time-critical clock
* thread - else through a lock, as video_driver.c's frame cache does. */
static vblank_clock_t d3dkmt_vc_w;
#if defined(HAVE_THREADS) && defined(VBLANK_CLOCK_HAS_PUB)
#define D3DKMT_VC_SEQLOCK 1
static vblank_clock_pub_t d3dkmt_vc_pub;
static void d3dkmt_vc_publish(void)
{
vblank_clock_publish(&d3dkmt_vc_pub, &d3dkmt_vc_w);
}
static bool d3dkmt_vc_read(vblank_clock_t *out)
{
return vblank_clock_snapshot(&d3dkmt_vc_pub, out);
}
#else
#ifdef HAVE_THREADS
static slock_t *d3dkmt_vc_lock;
#endif
static vblank_clock_t d3dkmt_vc_pub;
static void d3dkmt_vc_publish(void)
{
#ifdef HAVE_THREADS
if (d3dkmt_vc_lock)
slock_lock(d3dkmt_vc_lock);
#endif
d3dkmt_vc_pub = d3dkmt_vc_w;
#ifdef HAVE_THREADS
if (d3dkmt_vc_lock)
slock_unlock(d3dkmt_vc_lock);
#endif
}
static bool d3dkmt_vc_read(vblank_clock_t *out)
{
#ifdef HAVE_THREADS
if (d3dkmt_vc_lock)
slock_lock(d3dkmt_vc_lock);
#endif
*out = d3dkmt_vc_pub;
#ifdef HAVE_THREADS
if (d3dkmt_vc_lock)
slock_unlock(d3dkmt_vc_lock);
#endif
return true;
}
#endif
enum d3dkmt_source
{
D3DKMT_SRC_NONE = 0,
D3DKMT_SRC_DWM,
D3DKMT_SRC_KMT,
D3DKMT_SRC_DDRAW
};
static enum d3dkmt_source d3dkmt_source;
/* The mode's line counts and rate; 0 when unknown. */
static unsigned d3dkmt_active_lines;
static unsigned d3dkmt_total_lines;
static double d3dkmt_nominal_period_us;
/* The writer's operations - only ever on the current writer's thread. */
static void d3dkmt_vc_reset(void)
{
vblank_clock_init(&d3dkmt_vc_w, d3dkmt_nominal_period_us);
d3dkmt_vc_publish();
}
static void d3dkmt_vc_feed(int64_t when)
{
vblank_clock_feed(&d3dkmt_vc_w, when);
d3dkmt_vc_publish();
}
static void d3dkmt_vc_miss(void)
{
vblank_clock_miss(&d3dkmt_vc_w);
d3dkmt_vc_publish();
}
/* ---- the kernel vblank event --------------------------------------- */
#ifdef HAVE_THREADS
typedef struct d3dkmt_kmt
{
sthread_t *thread;
HANDLE stop_event;
D3DKMT_WAITFORVERTICALBLANKEVENT vb;
volatile int stop;
} d3dkmt_kmt_t;
static d3dkmt_kmt_t d3dkmt_kmt;
static void d3dkmt_kmt_thread(void *data)
{
d3dkmt_kmt_t *k = (d3dkmt_kmt_t*)data;
/* Its timestamps are the beam estimate: a late wake-up is a beam
* position off by that much. */
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
while (!k->stop)
{
NTSTATUS r = pD3DKMTWaitForVerticalBlankEvent(&k->vb);
if (k->stop)
break;
if (r != STATUS_SUCCESS)
{
/* The kernel's own 80 ms timeout, or the adapter gone: never
* a vblank. An error that returns at once must not become a
* busy loop, so back off on the stop event. */
d3dkmt_vc_miss();
WaitForSingleObject(k->stop_event, 100);
continue;
}
d3dkmt_vc_feed(cpu_features_get_time_usec());
}
}
/* On the thread that owns the adapter state, before the handle the
* thread waits on is closed. Bounded: the wait it joins returns at the
* next vblank or the kernel's 80 ms timeout. */
static void d3dkmt_kmt_stop(void)
{
d3dkmt_kmt_t *k = &d3dkmt_kmt;
if (!k->thread)
return;
k->stop = 1;
SetEvent(k->stop_event);
sthread_join(k->thread);
CloseHandle(k->stop_event);
memset(k, 0, sizeof(*k));
}
static bool d3dkmt_kmt_start(void)
{
d3dkmt_kmt_t *k = &d3dkmt_kmt;
if (k->thread)
return true;
if ( !pD3DKMTWaitForVerticalBlankEvent
|| !d3dkmt_adapter.vb.hAdapter)
return false;
memset(k, 0, sizeof(*k));
k->vb = d3dkmt_adapter.vb;
if (!(k->stop_event = CreateEvent(NULL, FALSE, FALSE, NULL)))
return false;
if (!(k->thread = sthread_create(d3dkmt_kmt_thread, k)))
{
CloseHandle(k->stop_event);
memset(k, 0, sizeof(*k));
return false;
}
return true;
}
#else
static void d3dkmt_kmt_stop(void) { }
static bool d3dkmt_kmt_start(void) { return false; }
#endif
/* ---- DWM ------------------------------------------------------------ */
typedef HRESULT (WINAPI *d3dkmt_dwm_timing_fn)(HWND, win32_dwm_timing_info_t*);
/* DWM's last vblank and refresh period, on cpu_features' QPC clock. */
static bool d3dkmt_dwm_timing(retro_time_t *last, double *period_us)
{
win32_dwm_timing_info_t info;
static d3dkmt_dwm_timing_fn get_timing;
static bool resolved;
static LARGE_INTEGER freq;
if (!resolved)
{
HMODULE dwm = LoadLibrary("dwmapi.dll");
resolved = true;
if (dwm)
get_timing = (d3dkmt_dwm_timing_fn)GetProcAddress(dwm,
"DwmGetCompositionTimingInfo");
}
if (!get_timing)
return false;
if (!freq.QuadPart && !QueryPerformanceFrequency(&freq))
return false;
memset(&info, 0, sizeof(info));
info.cbSize = sizeof(info);
if (FAILED(get_timing(NULL, &info)) || !info.qpcVBlank
|| !info.qpcRefreshPeriod)
return false;
*last = (retro_time_t)((info.qpcVBlank / freq.QuadPart * 1000000)
+ (info.qpcVBlank % freq.QuadPart * 1000000 / freq.QuadPart));
*period_us = (double)info.qpcRefreshPeriod * 1000000.0
/ (double)freq.QuadPart;
return true;
}
static retro_time_t d3dkmt_dwm_prev;
static unsigned d3dkmt_dwm_live;
/* Frames DWM's timing must keep advancing before it takes over from
* the kernel event, so a window flicking in and out of exclusive
* fullscreen does not keep restarting the clock. */
#define D3DKMT_DWM_TAKEOVER 60
/* ---- DirectDraw, XDDM ----------------------------------------------- */
typedef HRESULT (WINAPI *d3dkmt_ddcreate_fn)(GUID FAR*, LPDIRECTDRAW FAR*,
IUnknown FAR*);
typedef HRESULT (WINAPI *d3dkmt_ddenumex_fn)(LPDDENUMCALLBACKEXA, LPVOID,
DWORD);
static d3dkmt_ddcreate_fn d3dkmt_DirectDrawCreate;
static d3dkmt_ddenumex_fn d3dkmt_DirectDrawEnumerateExA;
static LPDIRECTDRAW d3dkmt_dd;
static vblank_sampler_t d3dkmt_smp;
/* Readings in a row that were neither a line nor "in blanking": a
* driver without GetScanLine. */
static unsigned d3dkmt_dd_fails;
typedef struct d3dkmt_dd_find
{
HMONITOR mon;
GUID guid;
bool found;
} d3dkmt_dd_find_t;
static BOOL WINAPI d3dkmt_dd_enum_cb(GUID FAR *guid, LPSTR desc,
LPSTR name, LPVOID ctx, HMONITOR mon)
{
d3dkmt_dd_find_t *f = (d3dkmt_dd_find_t*)ctx;
(void)desc; (void)name;
if (guid && mon && mon == f->mon)
{
f->guid = *guid;
f->found = true;
return FALSE;
}
return TRUE;
}
static void d3dkmt_dd_close(void)
{
if (d3dkmt_dd)
d3dkmt_dd->lpVtbl->Release(d3dkmt_dd);
d3dkmt_dd = NULL;
d3dkmt_dd_fails = 0;
memset(&d3dkmt_smp, 0, sizeof(d3dkmt_smp));
}
/* The DirectDraw object for @mon's display - by the GUID DirectDraw
* enumerates for it on a multi-monitor desktop, else the primary - and
* the mode's active lines and whole-Hz rate from EnumDisplaySettings,
* which is all XDDM reports; the sampler measures the rest. */
static bool d3dkmt_dd_open(HMONITOR mon)
{
MONITORINFOEX mi;
DEVMODE dm;
d3dkmt_dd_find_t f;
d3dkmt_dd_close();
if (!d3dkmt_DirectDrawCreate)
{
HMODULE dd = LoadLibrary("ddraw.dll");
if (!dd)
return false;
d3dkmt_DirectDrawCreate = (d3dkmt_ddcreate_fn)
GetProcAddress(dd, "DirectDrawCreate");
d3dkmt_DirectDrawEnumerateExA = (d3dkmt_ddenumex_fn)
GetProcAddress(dd, "DirectDrawEnumerateExA");
if (!d3dkmt_DirectDrawCreate)
return false;
}
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
memset(&dm, 0, sizeof(dm));
dm.dmSize = sizeof(dm);
if ( !mon
|| !GetMonitorInfo(mon, (LPMONITORINFO)&mi)
|| !EnumDisplaySettings(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)
|| !dm.dmPelsHeight)
return false;
memset(&f, 0, sizeof(f));
f.mon = mon;
if (d3dkmt_DirectDrawEnumerateExA)
d3dkmt_DirectDrawEnumerateExA(d3dkmt_dd_enum_cb, &f,
DDENUM_ATTACHEDSECONDARYDEVICES);
if (FAILED(d3dkmt_DirectDrawCreate(f.found ? &f.guid : NULL,
&d3dkmt_dd, NULL)))
{
d3dkmt_dd = NULL;
return false;
}
d3dkmt_monitor = mon;
d3dkmt_active_lines = dm.dmPelsHeight;
d3dkmt_total_lines = 0; /* measured by the sampler */
/* 0 and 1 mean "hardware default", which some drivers report for
* every mode: start from 60 Hz. The clock only takes vblanks within
* a tenth of the period it expects, so a display far from that never
* settles and Scanline Sync idles - it does not race a wrong beam. */
d3dkmt_nominal_period_us = 1000000.0 / (double)
((dm.dmDisplayFrequency > 1) ? dm.dmDisplayFrequency : 60);
video_driver_scanline_init();
d3dkmt_vc_reset();
d3dkmt_source = D3DKMT_SRC_DDRAW;
return true;
}
/* One reading, into the sampler, and the vblank it implies into the
* clock. */
static void d3dkmt_dd_sample(void)
{
vblank_clock_t vc;
DWORD line = 0;
unsigned total = 0;
retro_time_t t0, t1;
int64_t vb;
HRESULT hr;
t0 = cpu_features_get_time_usec();
hr = d3dkmt_dd->lpVtbl->GetScanLine(d3dkmt_dd, &line);
t1 = cpu_features_get_time_usec();
if (hr != DD_OK && hr != DDERR_VERTICALBLANKINPROGRESS)
{
/* No GetScanLine in this driver: stop asking. */
if (++d3dkmt_dd_fails >= 60)
{
d3dkmt_dd_close();
d3dkmt_source = D3DKMT_SRC_NONE;
}
return;
}
d3dkmt_dd_fails = 0;
/* The frame thread is this source's writer: its own copy. */
vc = d3dkmt_vc_w;
vb = vblank_sampler_feed(&d3dkmt_smp, t0 + (t1 - t0) / 2,
(hr == DD_OK) ? (int)line : -1, d3dkmt_active_lines,
(vc.good >= VBLANK_CLOCK_SETTLE) ? vc.period_us
: d3dkmt_nominal_period_us,
&total);
if (vb)
{
d3dkmt_total_lines = total;
d3dkmt_vc_feed(vb);
}
}
/* ---- the adapter and its timing (WDDM) ------------------------------- */
static void d3dkmt_close(void)
{
/* The kernel-event thread waits on the adapter handle: stop it
* first. */
d3dkmt_kmt_stop();
d3dkmt_dd_close();
if (pD3DKMTCloseAdapter && d3dkmt_adapter.sl.hAdapter)
{
D3DKMT_CLOSEADAPTER ca;
ca.hAdapter = d3dkmt_adapter.sl.hAdapter;
pD3DKMTCloseAdapter(&ca);
}
memset(&d3dkmt_adapter, 0, sizeof(d3dkmt_adapter_t));
d3dkmt_monitor = NULL;
d3dkmt_active_lines = 0;
d3dkmt_total_lines = 0;
d3dkmt_nominal_period_us = 0.0;
d3dkmt_source = D3DKMT_SRC_NONE;
d3dkmt_dwm_live = 0;
d3dkmt_dwm_prev = 0;
}
/* The line counts and rate of the mode the counter's output is driving,
* from QueryDisplayConfig (Windows 7 and later). A change restarts the
* tuner's calibration and the clock. */
static void d3dkmt_read_timing(LUID luid, UINT32 source_id)
{
unsigned active = 0, total = 0;
double hz = 0.0;
if (!win32_display_signal_timing(luid, source_id, &active, &total, &hz)
|| hz <= 0.0)
active = total = 0;
if ( active != d3dkmt_active_lines
|| total != d3dkmt_total_lines
|| ((hz > 0.0) ? 1000000.0 / hz : 0.0) != d3dkmt_nominal_period_us)
{
video_driver_scanline_init();
d3dkmt_kmt_stop();
d3dkmt_active_lines = active;
d3dkmt_total_lines = total;
d3dkmt_nominal_period_us = (hz > 0.0) ? 1000000.0 / hz : 0.0;
d3dkmt_vc_reset();
d3dkmt_source = D3DKMT_SRC_NONE; /* picked again */
d3dkmt_dwm_live = 0;
}
}
/* Opens the scanline counter and vblank wait on the adapter output
* driving @mon and reads the mode's timing. It used to open whichever
* display device answered first - usually the primary - whatever
* monitor the window was on, so on a second screen scanline sync raced
* another display's beam. */
static bool d3dkmt_open_monitor(HMONITOR mon)
{
MONITORINFOEX mi;
HDC hdc;
D3DKMT_OPENADAPTERFROMHDC oa;
if (!mon)
return false;
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
if (!GetMonitorInfo(mon, (LPMONITORINFO)&mi))
return false;
if (!(hdc = CreateDC(NULL, mi.szDevice, NULL, NULL)))
return false;
memset(&oa, 0, sizeof(oa));
oa.hDc = hdc;
if (pD3DKMTOpenAdapterFromHdc(&oa) != STATUS_SUCCESS)
{
DeleteDC(hdc);
return false;
}
DeleteDC(hdc);
d3dkmt_close();
d3dkmt_adapter.sl.hAdapter = oa.hAdapter;
d3dkmt_adapter.sl.VidPnSourceId = oa.VidPnSourceId;
/* hDevice is documented optional and is not needed to wait on a
* VidPn source. */
d3dkmt_adapter.vb.hAdapter = oa.hAdapter;
d3dkmt_adapter.vb.VidPnSourceId = oa.VidPnSourceId;
d3dkmt_adapter.luid = oa.AdapterLuid;
d3dkmt_monitor = mon;
d3dkmt_read_timing(oa.AdapterLuid, oa.VidPnSourceId);
return true;
}
/* ---- per call: pick the source, feed the clock --------------------- */
static void d3dkmt_tick(void)
{
retro_time_t now, last = 0;
double dwm_period = 0.0;
bool dwm_ok = false;
if (d3dkmt_source == D3DKMT_SRC_DDRAW)
{
if (d3dkmt_dd)
d3dkmt_dd_sample();
return;
}
/* WDDM, with the mode's line counts (not Vista) - else the counter. */
if (!d3dkmt_adapter.sl.hAdapter || !d3dkmt_total_lines)
return;
now = cpu_features_get_time_usec();
if ( d3dkmt_dwm_timing(&last, &dwm_period)
&& fabs(dwm_period - d3dkmt_nominal_period_us)
< 0.005 * d3dkmt_nominal_period_us
&& now - last < (retro_time_t)(3.0 * dwm_period))
dwm_ok = true;
if (!dwm_ok)
d3dkmt_dwm_live = 0;
else if (last != d3dkmt_dwm_prev)
d3dkmt_dwm_live++;
if (d3dkmt_source == D3DKMT_SRC_NONE)
{
if (dwm_ok)
d3dkmt_source = D3DKMT_SRC_DWM;
else if (d3dkmt_kmt_start())
d3dkmt_source = D3DKMT_SRC_KMT;
}
else if (d3dkmt_source == D3DKMT_SRC_DWM && !dwm_ok)
{
/* Exclusive fullscreen, composition off, or DWM pacing another
* display: the kernel event. */
d3dkmt_vc_reset();
d3dkmt_source = d3dkmt_kmt_start() ? D3DKMT_SRC_KMT
: D3DKMT_SRC_NONE;
}
else if ( d3dkmt_source == D3DKMT_SRC_KMT
&& d3dkmt_dwm_live >= D3DKMT_DWM_TAKEOVER)
{
d3dkmt_kmt_stop();
d3dkmt_vc_reset();
d3dkmt_source = D3DKMT_SRC_DWM;
}
if (d3dkmt_source == D3DKMT_SRC_DWM && last != d3dkmt_dwm_prev)
d3dkmt_vc_feed(last);
if (dwm_ok)
d3dkmt_dwm_prev = last;
}
/* Before a read, on the reading thread: follow the window to the
* monitor it is on now, if it may have moved or the mode changed.
* Cheap when nothing changed - one atomic load. */
static void d3dkmt_follow_window(void)
{
HMONITOR mon;
if (retro_atomic_load_acquire_int(&d3dkmt_rebind_pending))
{
retro_atomic_store_release_int(&d3dkmt_rebind_pending, 0);
if (main_window.hwnd)
{
mon = MonitorFromWindow(main_window.hwnd, MONITOR_DEFAULTTONEAREST);
if (!pD3DKMTOpenAdapterFromHdc)
{
/* XDDM: DirectDraw, re-opened for a new monitor or mode. */
DEVMODE dm;
MONITORINFOEX mi;
bool changed = (mon != d3dkmt_monitor);
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
memset(&dm, 0, sizeof(dm));
dm.dmSize = sizeof(dm);
if ( !changed
&& GetMonitorInfo(mon, (LPMONITORINFO)&mi)
&& EnumDisplaySettings(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)
&& ( dm.dmPelsHeight != d3dkmt_active_lines
|| 1000000.0 / (double)((dm.dmDisplayFrequency > 1)
? dm.dmDisplayFrequency : 60)
!= d3dkmt_nominal_period_us))
changed = true;
if (changed || !d3dkmt_dd)
if (!d3dkmt_dd_open(mon))
d3dkmt_source = D3DKMT_SRC_NONE;
}
else if (mon && mon != d3dkmt_monitor)
d3dkmt_open_monitor(mon);
else if (mon && d3dkmt_adapter.sl.hAdapter)
/* Same monitor, perhaps a new mode. */
d3dkmt_read_timing(d3dkmt_adapter.luid,
d3dkmt_adapter.sl.VidPnSourceId);
}
}
d3dkmt_tick();
}
static void d3dkmt_request_rebind(void)
{
retro_atomic_store_release_int(&d3dkmt_rebind_pending, 1);
if (d3dkmt_wake_event)
SetEvent(d3dkmt_wake_event);
}
static void d3dkmt_init(void)
{
if (!pD3DKMTOpenAdapterFromHdc)
if (!d3dkmt_wake_event)
d3dkmt_wake_event = CreateEvent(NULL, FALSE, FALSE, NULL);
#if defined(HAVE_THREADS) && !defined(D3DKMT_VC_SEQLOCK)
if (!d3dkmt_vc_lock)
d3dkmt_vc_lock = slock_new();
#endif
if (!pD3DKMTOpenAdapterFromHdc && !d3dkmt_DirectDrawCreate)
{
unsigned d3dkmt_adapter_hAdapter = 0;
unsigned d3dkmt_adapter_VidPnSourceId = 0;
unsigned adapter_index = 0;
DISPLAY_DEVICE add;
add.cb = sizeof(add);
HMODULE gdi32 = GetModuleHandle("gdi32.dll");
pD3DKMTOpenAdapterFromHdc = (D3DKMTOPENADAPTERFROMHDC)
GetProcAddress(GetModuleHandle("gdi32.dll"), "D3DKMTOpenAdapterFromHdc");
GetProcAddress(gdi32, "D3DKMTOpenAdapterFromHdc");
pD3DKMTGetScanLine = (D3DKMTGETSCANLINE)
GetProcAddress(GetModuleHandle("gdi32.dll"), "D3DKMTGetScanLine");
/* Optional: only the phase anchor depends on it, and
* d3dkmt_wait_vblank() reports its absence so the caller can
* fall back. Not part of the guard below for that reason. */
GetProcAddress(gdi32, "D3DKMTGetScanLine");
pD3DKMTWaitForVerticalBlankEvent = (D3DKMTWAITFORVERTICALBLANKEVENT)
GetProcAddress(GetModuleHandle("gdi32.dll"),
"D3DKMTWaitForVerticalBlankEvent");
GetProcAddress(gdi32, "D3DKMTWaitForVerticalBlankEvent");
/* Optional: without it an adapter handle leaks on rebind, as
* every one did before. */
pD3DKMTCloseAdapter = (D3DKMTCLOSEADAPTER_FN)
GetProcAddress(gdi32, "D3DKMTCloseAdapter");
/* Both exports are WDDM, so they are absent under XDDM - there
* are no D3DKMT entry points in gdi32 before Vista at all.
* pD3DKMTGetScanLine is null-checked at its two use sites;
* pD3DKMTOpenAdapterFromHdc was not, and it is called inside the
* loop below, so a missing export was a call through NULL on the
* first display device. Leave the scanline state zeroed and let
* d3dkmt_scanline_get() report -1, which
* video_driver_scanline_before_frame() already treats as
* unsupported. */
if (!pD3DKMTOpenAdapterFromHdc || !pD3DKMTGetScanLine)
{
memset(&d3dkmt_adapter, 0, sizeof(d3dkmt_adapter_t));
video_driver_scanline_init();
return;
}
POINT origin;
HMONITOR primary;
origin.x = 0;
origin.y = 0;
primary = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
while (EnumDisplayDevices(NULL, adapter_index, &add, 0))
{
HDC hdc = CreateDC(NULL, add.DeviceName, NULL, NULL);
if (hdc != NULL)
if (!pD3DKMTOpenAdapterFromHdc || !pD3DKMTGetScanLine)
{
D3DKMT_OPENADAPTERFROMHDC OpenAdapterData = {0};
OpenAdapterData.hDc = hdc;
if (pD3DKMTOpenAdapterFromHdc(&OpenAdapterData) == STATUS_SUCCESS)
{
d3dkmt_adapter_hAdapter = OpenAdapterData.hAdapter;
d3dkmt_adapter_VidPnSourceId = OpenAdapterData.VidPnSourceId;
}
DeleteDC(hdc);
if (d3dkmt_adapter_hAdapter)
break;
/* No WDDM - Windows 2000 and XP, or an XDDM driver: the
* DirectDraw readings. Nothing there either, and the
* scanline reads as unsupported. */
pD3DKMTOpenAdapterFromHdc = NULL;
memset(&d3dkmt_adapter, 0, sizeof(d3dkmt_adapter_t));
d3dkmt_dd_open(primary);
}
adapter_index++;
}
memset(&d3dkmt_adapter, 0, sizeof(d3dkmt_adapter_t));
if (pD3DKMTGetScanLine)
{
D3DKMT_GETSCANLINE sl = {0};
sl.hAdapter = d3dkmt_adapter_hAdapter;
sl.VidPnSourceId = d3dkmt_adapter_VidPnSourceId;
d3dkmt_adapter.sl = sl;
}
{
D3DKMT_WAITFORVERTICALBLANKEVENT vb = {0};
vb.hAdapter = d3dkmt_adapter_hAdapter;
vb.VidPnSourceId = d3dkmt_adapter_VidPnSourceId;
/* hDevice is documented optional and is not needed to wait on
* a VidPn source. */
d3dkmt_adapter.vb = vb;
else
/* No window yet at class registration: the primary monitor,
* as a start. The window's first move or show rebinds it to
* the monitor the window is actually on. */
d3dkmt_open_monitor(primary);
}
}
d3dkmt_request_rebind();
video_driver_scanline_init();
}
bool d3dkmt_wait_vblank(void)
{
d3dkmt_follow_window();
if (!pD3DKMTWaitForVerticalBlankEvent || !d3dkmt_adapter.vb.hAdapter)
return false;
return (pD3DKMTWaitForVerticalBlankEvent(&d3dkmt_adapter.vb)
@@ -339,13 +926,102 @@ bool d3dkmt_wait_vblank(void)
int d3dkmt_scanline_get(void)
{
if (pD3DKMTGetScanLine)
vblank_clock_t vc;
int beam;
d3dkmt_follow_window();
if (d3dkmt_source != D3DKMT_SRC_NONE)
{
/* A clock source: the beam once it has settled, and until then
* nothing - Scanline Sync idles rather than polling. */
if (!d3dkmt_total_lines || !d3dkmt_vc_read(&vc))
return -1;
beam = vblank_clock_beam(&vc, cpu_features_get_time_usec(),
d3dkmt_active_lines, d3dkmt_total_lines);
return beam;
}
/* No clock can serve this display (Vista): the counter. */
if ( pD3DKMTGetScanLine
&& d3dkmt_adapter.sl.hAdapter
&& !d3dkmt_total_lines)
{
if (pD3DKMTGetScanLine(&d3dkmt_adapter.sl) == STATUS_SUCCESS)
return d3dkmt_adapter.sl.ScanLine;
}
return -1;
}
int d3dkmt_scanline_height(void)
{
return (int)d3dkmt_active_lines;
}
int d3dkmt_scanline_total(void)
{
return (int)d3dkmt_total_lines;
}
/* The frame thread's high-resolution timer, made on first use. */
static HANDLE d3dkmt_frame_timer;
typedef HANDLE (WINAPI *CREATEWAITABLETIMEREXW_FN)(LPSECURITY_ATTRIBUTES,
LPCWSTR, DWORD, DWORD);
static HANDLE d3dkmt_timer_get(void)
{
if (!d3dkmt_frame_timer)
{
/* CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Windows 10 1803+), else
* a plain one: coarser, but still a timer, not a poll. */
CREATEWAITABLETIMEREXW_FN create = (CREATEWAITABLETIMEREXW_FN)
GetProcAddress(GetModuleHandle("kernel32.dll"),
"CreateWaitableTimerExW");
if (create)
d3dkmt_frame_timer = create(NULL, NULL, 0x00000002,
TIMER_ALL_ACCESS);
if (!d3dkmt_frame_timer)
d3dkmt_frame_timer = CreateWaitableTimer(NULL, FALSE, NULL);
}
return d3dkmt_frame_timer;
}
bool d3dkmt_scanline_wait(int target_line, unsigned max_us)
{
HANDLE handles[2];
LARGE_INTEGER due;
vblank_clock_t vc;
int64_t wait_us;
DWORD n = 1;
d3dkmt_follow_window();
if ( d3dkmt_source == D3DKMT_SRC_NONE
|| !d3dkmt_total_lines
|| !d3dkmt_vc_read(&vc))
return false;
if ((wait_us = vblank_clock_until_line(&vc, cpu_features_get_time_usec(),
d3dkmt_active_lines, d3dkmt_total_lines, target_line)) < 0)
return false;
if (wait_us > (int64_t)max_us)
wait_us = (int64_t)max_us;
/* The beam is already there, as the counter loop would have found. */
if (wait_us < 1)
return true;
if (!(handles[0] = d3dkmt_timer_get()))
return false;
/* Relative, in 100 ns units. */
due.QuadPart = -(LONGLONG)(wait_us * 10);
if (!SetWaitableTimer(handles[0], &due, 0, NULL, NULL, FALSE))
return false;
if (d3dkmt_wake_event)
handles[n++] = d3dkmt_wake_event;
/* The timer, or a rebind (new monitor, new mode) ending it early;
* bounded past the timer's due time by one more wait's worth. */
WaitForMultipleObjects(n, handles, FALSE,
(DWORD)(wait_us / 1000) + 1 + max_us / 1000);
CancelWaitableTimer(handles[0]);
return true;
}
#endif /* HAVE_D3DKMT */
typedef struct win32_common_state
@@ -901,6 +1577,9 @@ static LRESULT CALLBACK wnd_proc_common(
/* fall-through */
case WM_MOVE:
win32_save_position();
#ifdef HAVE_D3DKMT
d3dkmt_request_rebind();
#endif
break;
#if !defined(_XBOX)
case WM_ENTERSIZEMOVE:
@@ -1175,6 +1854,9 @@ static LRESULT CALLBACK wnd_proc_common_internal(HWND hwnd,
#endif
break;
case WM_DISPLAYCHANGE: /* Fix size after display mode switch when using SR */
#ifdef HAVE_D3DKMT
d3dkmt_request_rebind();
#endif
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (mon)
@@ -1284,6 +1966,9 @@ static LRESULT CALLBACK wnd_proc_winraw_common_internal(HWND hwnd,
#endif
break;
case WM_DISPLAYCHANGE: /* Fix size after display mode switch when using SR */
#ifdef HAVE_D3DKMT
d3dkmt_request_rebind();
#endif
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (mon)
@@ -1523,6 +2208,9 @@ static LRESULT CALLBACK wnd_proc_common_dinput_internal(HWND hwnd,
#endif
break;
case WM_DISPLAYCHANGE: /* Fix size after display mode switch when using SR */
#ifdef HAVE_D3DKMT
d3dkmt_request_rebind();
#endif
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (mon)
+23
View File
@@ -279,10 +279,33 @@ typedef struct d3dkmt_adapter
{
D3DKMT_GETSCANLINE sl;
D3DKMT_WAITFORVERTICALBLANKEVENT vb;
/* The adapter's LUID, which with the VidPn source ID identifies the
* display path to QueryDisplayConfig(). */
LUID luid;
} d3dkmt_adapter_t;
extern int d3dkmt_scanline_get(void);
/* Active and total lines of the display mode the scanline counter is
* on, from its signal timing, or 0 when not known. These, not the
* window or viewport height, are the units get_scanline() counts in. */
extern int d3dkmt_scanline_height(void);
extern int d3dkmt_scanline_total(void);
/* Blocks until the beam reaches @target_line of the display the counter
* is on, or @max_us passes, on a high-resolution timer aimed from the
* vblank clock - no polling, no sleep. Returns false when the clock is
* not running (no vblank event, unknown timing, an interlaced or
* variable-rate display), and the caller falls back to reading the
* counter. */
extern bool d3dkmt_scanline_wait(int target_line, unsigned max_us);
/* In dispserv_win32.c: the progressive signal timing of the display
* path an adapter output drives, by the LUID and VidPn source ID that
* D3DKMTOpenAdapterFromHdc() reports. */
extern bool win32_display_signal_timing(LUID adapter, UINT32 source_id,
unsigned *active_lines, unsigned *total_lines, double *refresh_hz);
/* Block until the display signals vertical blank. Returns false when
* the entry point is unavailable or the wait fails, in which case the
* caller has no anchor and must fall back to polling.
+81
View File
@@ -837,6 +837,87 @@ static float win32_display_server_get_refresh_rate(void *data)
#endif
}
/* The signal timing of the display path driven by one adapter output,
* identified the way D3DKMTOpenAdapterFromHdc() reports it: the
* adapter LUID and the VidPn source ID, which are the adapterId and
* source id QueryDisplayConfig() uses. Scanline Sync needs the active
* and total line counts to turn a vblank timestamp into a beam
* position. Progressive modes only: an interlaced beam is not a
* single line count per refresh. */
bool win32_display_signal_timing(LUID adapter, UINT32 source_id,
unsigned *active_lines, unsigned *total_lines, double *refresh_hz)
{
#if _WIN32_WINNT >= 0x0601 || _WIN32_WINDOWS >= 0x0601 /* Win 7 */
UINT32 i;
UINT32 num_paths = 0;
UINT32 num_modes = 0;
bool found = false;
DISPLAYCONFIG_PATH_INFO_CUSTOM *paths = NULL;
DISPLAYCONFIG_MODE_INFO_CUSTOM *modes = NULL;
#ifdef HAVE_DYLIB
static QUERYDISPLAYCONFIG pQDC;
static GETDISPLAYCONFIGBUFFERSIZES pGDCBS;
if (!pQDC || !pGDCBS)
{
HMODULE user32 = GetModuleHandle("user32.dll");
pQDC = (QUERYDISPLAYCONFIG)GetProcAddress(user32, "QueryDisplayConfig");
pGDCBS = (GETDISPLAYCONFIGBUFFERSIZES)GetProcAddress(user32,
"GetDisplayConfigBufferSizes");
}
#else
static QUERYDISPLAYCONFIG pQDC = QueryDisplayConfig;
static GETDISPLAYCONFIGBUFFERSIZES pGDCBS = GetDisplayConfigBufferSizes;
#endif
if (!pQDC || !pGDCBS)
return false;
/* QDC_ONLY_ACTIVE_PATHS: the timing on the wire now. */
if (pGDCBS(0x2, &num_paths, &num_modes) != ERROR_SUCCESS
|| !num_paths || !num_modes)
return false;
paths = (DISPLAYCONFIG_PATH_INFO_CUSTOM*)calloc(num_paths, sizeof(*paths));
modes = (DISPLAYCONFIG_MODE_INFO_CUSTOM*)calloc(num_modes, sizeof(*modes));
if ( paths && modes
&& pQDC(0x2, &num_paths, paths, &num_modes, modes, NULL)
== ERROR_SUCCESS)
{
for (i = 0; i < num_paths && !found; i++)
{
const DISPLAYCONFIG_PATH_INFO_CUSTOM *p = &paths[i];
UINT32 m = p->targetInfo.dummyunionname.modeInfoIdx;
const DISPLAYCONFIG_VIDEO_SIGNAL_INFO_CUSTOM *sig;
if ( p->sourceInfo.adapterId.LowPart != adapter.LowPart
|| p->sourceInfo.adapterId.HighPart != adapter.HighPart
|| p->sourceInfo.id != source_id)
continue;
/* DISPLAYCONFIG_MODE_INFO_TYPE_TARGET */
if (m >= num_modes || modes[m].infoType != 2)
continue;
sig = &modes[m].dummyunionname.targetMode.targetVideoSignalInfo;
/* DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE */
if ( sig->scanLineOrdering != 1
|| !sig->activeSize.cy
|| sig->totalSize.cy <= sig->activeSize.cy
|| !sig->vSyncFreq.Denominator
|| !sig->vSyncFreq.Numerator)
continue;
*active_lines = sig->activeSize.cy;
*total_lines = sig->totalSize.cy;
*refresh_hz = (double)sig->vSyncFreq.Numerator
/ (double)sig->vSyncFreq.Denominator;
found = true;
}
}
free(paths);
free(modes);
return found;
#else
return false;
#endif
}
static void win32_display_server_get_video_output_size(void *data,
unsigned *width, unsigned *height, char *s, size_t len)
{
+69 -5
View File
@@ -7754,6 +7754,22 @@ void video_driver_scanline_init(void)
video_st->scanline[SCANLINE_TOTAL] = 0;
}
/* Scanlines are counted in lines of the display's current mode. The
* tuner used to measure them against RetroArch's viewport height,
* which is only the same thing fullscreen at the desktop resolution:
* in a window its targets and its estimate of the blanking interval
* came out in the wrong units. The viewport height stays the fallback
* where the mode's timing is not known. */
static INLINE uint16_t video_driver_scanline_height(video_driver_state_t *video_st)
{
#ifdef HAVE_D3DKMT
int height = d3dkmt_scanline_height();
if (height > 0)
return (uint16_t)height;
#endif
return (uint16_t)VIDEO_DRIVER_OUTPUT_HEIGHT(VIDEO_DRIVER_OUTPUT_SIZE(video_st));
}
/* The beam position through the display server; a server without
* get_scanline() reports -1 and the tuner disables itself below. */
static INLINE int16_t video_driver_scanline_get(void)
@@ -7766,8 +7782,7 @@ VIDEO_NOINLINE static void video_driver_scanline_before_frame(video_driver_state
uint16_t frame_time_target,
uint16_t core_run_time)
{
uint16_t video_height = (uint16_t)VIDEO_DRIVER_OUTPUT_HEIGHT(
VIDEO_DRIVER_OUTPUT_SIZE(video_st));
uint16_t video_height = video_driver_scanline_height(video_st);
int16_t scanline_next = video_st->scanline[SCANLINE_NEXT];
int16_t scanline_hold = video_st->scanline[SCANLINE_HOLD];
int16_t scanline_blank = video_st->scanline[SCANLINE_TOTAL] - video_height;
@@ -7850,8 +7865,7 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
uint16_t frame_time_target,
uint16_t core_run_time)
{
uint16_t video_height = (uint16_t)VIDEO_DRIVER_OUTPUT_HEIGHT(
VIDEO_DRIVER_OUTPUT_SIZE(video_st));
uint16_t video_height = video_driver_scanline_height(video_st);
int16_t scanline_next = video_st->scanline[SCANLINE_NEXT];
int16_t scanline_total = video_st->scanline[SCANLINE_TOTAL];
int16_t scanline_blank = video_st->scanline[SCANLINE_TOTAL] - video_height;
@@ -7860,6 +7874,20 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
uint16_t min_run_time = (scanline_blank > 0) ? (double)scanline_blank / (double)video_height * (double)frame_time_target : 1000;
bool init = (!scanline_total) ? true : false;
bool wait = true;
retro_time_t spin_until = 0;
#ifdef HAVE_D3DKMT
/* The mode's timing gives the total line count outright; the
* calibration pass below measured it by watching a whole frame. */
if (init && d3dkmt_scanline_total() > video_height)
{
scanline_total = (int16_t)d3dkmt_scanline_total();
scanline_blank = scanline_total - video_height;
min_run_time = (double)scanline_blank / (double)video_height
* (double)frame_time_target;
init = false;
}
#endif
if ( scanline_target <= 0
|| scanline_target >= video_height)
@@ -7872,7 +7900,24 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
/* Minimum usage is vblank */
core_run_time = (core_run_time < min_run_time) ? min_run_time : core_run_time;
/* Use CPU friendlier sleep as much as possible */
#ifdef HAVE_D3DKMT
/* The vblank clock: one timer aimed at the moment the beam reaches
* the target line, no reads of the counter while waiting and no
* sleep. It declines - and the counter loop below runs as before -
* when there is no clock: no vblank event, unknown or interlaced
* timing, or a period that will not settle (variable refresh). */
if ( wait && !init
&& d3dkmt_scanline_wait(scanline_target,
2 * (unsigned)frame_time_target))
{
int16_t landed = video_driver_scanline_get();
video_st->scanline[SCANLINE_NEXT] = (landed >= 0) ? landed : scanline_next;
video_st->scanline[SCANLINE_TOTAL] = scanline_total;
return;
}
#endif
/* No clock: the counter itself, as before. */
if (wait && frame_time_target > core_run_time)
{
int8_t sleep = (frame_time_target - core_run_time) / 1000;
@@ -7886,10 +7931,29 @@ VIDEO_NOINLINE static void video_driver_scanline_after_frame(video_driver_state_
}
}
/* Bounded: the only other way out is the beam reaching the target,
* and a query that starts failing mid-wait (display asleep, mode
* change, adapter reset) returns -1, which never does - the loop
* used to spin the frame thread at full CPU for good. Two frames
* covers the target plus the init pass measuring a whole frame. */
if (wait)
spin_until = cpu_features_get_time_usec()
+ 2 * (retro_time_t)frame_time_target;
while (wait)
{
scanline = video_driver_scanline_get();
if (scanline < 0)
{
/* Nothing to wait on; leave the tuner's state as it was. */
scanline = video_st->scanline[SCANLINE_NEXT];
break;
}
if (cpu_features_get_time_usec() >= spin_until)
break;
if (scanline >= scanline_target)
wait = false;
+40
View File
@@ -0,0 +1,40 @@
TARGET := vblank_clock_test
PUB := vblank_clock_pub_test
RARCH_DIR := ../../..
LIBRETRO_COMM_DIR := $(RARCH_DIR)/libretro-common
# gfx/common/vblank_clock.h is header-only arithmetic; no platform code.
SOURCES := vblank_clock_test.c
CFLAGS += -Wall -std=c89 -pedantic -Wno-long-long -g -O1 \
-I$(RARCH_DIR) -I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lm
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
ifneq ($(SAN),)
CFLAGS := -fsanitize=$(SAN) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SAN) $(LDFLAGS)
endif
all: $(TARGET) $(PUB)
$(TARGET): $(SOURCES)
$(CC) $(CFLAGS) -o $@ $(SOURCES) $(LDFLAGS)
# The seqlock between two threads: gnu99 for pthreads, run under
# SANITIZER=thread in CI.
$(PUB): vblank_clock_pub_test.c $(RARCH_DIR)/gfx/common/vblank_clock.h
$(CC) $(subst -std=c89 -pedantic,-std=gnu99,$(CFLAGS)) -o $@ vblank_clock_pub_test.c $(LDFLAGS) -lpthread
check: all
./$(TARGET)
./$(PUB)
clean:
rm -f $(TARGET) $(PUB)
.PHONY: all check clean
@@ -0,0 +1,118 @@
/* Test for vblank_clock_publish() / vblank_clock_snapshot() in
* gfx/common/vblank_clock.h: the seqlock Scanline Sync on Windows uses
* to hand the vblank clock from the kernel-event thread to the frame
* thread without a lock.
*
* One writer publishes as fast as it can, each clock built so its four
* fields agree with each other (all derived from one counter); readers
* on other threads snapshot as fast as they can and check that every
* snapshot they are given is one the writer published whole - never
* fields from two publishes. Run under ThreadSanitizer in CI, which
* also checks the pattern is free of formal data races.
*
* every snapshot is internally consistent
* snapshots move forward, never back
* readers get snapshots (the retry bound does not starve them)
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "gfx/common/vblank_clock.h"
#ifndef VBLANK_CLOCK_HAS_PUB
int main(void) { printf("no lock-free 64-bit atomics here: skipped\n"); return 0; }
#else
#define PUBLISHES 2000000
#define READERS 3
static vblank_clock_pub_t pub;
static retro_atomic_int_t writer_done;
static void make(vblank_clock_t *c, int64_t n)
{
c->last_us = 1000000 + n * 16667;
c->period_us = 16000.0 + (double)(n % 1000);
c->nominal_us = c->period_us * 2.0;
c->good = (unsigned)(n % 997);
}
static void *writer(void *arg)
{
int64_t n;
vblank_clock_t c;
(void)arg;
for (n = 1; n <= PUBLISHES; n++)
{
make(&c, n);
vblank_clock_publish(&pub, &c);
}
retro_atomic_store_release_int(&writer_done, 1);
return NULL;
}
typedef struct { long got, torn, backwards, starved; } result_t;
static void *reader(void *arg)
{
result_t *r = (result_t*)arg;
int64_t prev = 0;
while (!retro_atomic_load_acquire_int(&writer_done))
{
vblank_clock_t s, want;
int64_t n;
if (!vblank_clock_snapshot(&pub, &s))
{
r->starved++;
continue;
}
r->got++;
n = (s.last_us - 1000000) / 16667;
make(&want, n);
if ( s.last_us != want.last_us
|| s.period_us != want.period_us
|| s.nominal_us != want.nominal_us
|| s.good != want.good)
r->torn++;
if (n < prev)
r->backwards++;
prev = n;
}
return NULL;
}
int main(void)
{
pthread_t w, rd[READERS];
result_t res[READERS];
long got = 0, torn = 0, back = 0, starved = 0;
int i;
vblank_clock_t first;
make(&first, 0);
vblank_clock_publish(&pub, &first);
for (i = 0; i < READERS; i++)
{
res[i].got = res[i].torn = res[i].backwards = res[i].starved = 0;
pthread_create(&rd[i], NULL, reader, &res[i]);
}
pthread_create(&w, NULL, writer, NULL);
pthread_join(w, NULL);
for (i = 0; i < READERS; i++)
{
pthread_join(rd[i], NULL);
got += res[i].got; torn += res[i].torn;
back += res[i].backwards; starved += res[i].starved;
}
printf("%d publishes; readers took %ld snapshots, %ld gave up a try-bound, "
"%ld torn, %ld went backwards\n", PUBLISHES, got, starved, torn, back);
if (torn || back || !got)
{
printf("FAIL\n");
return 1;
}
printf("all passed\n");
return 0;
}
#endif
@@ -0,0 +1,266 @@
/* Test for gfx/common/vblank_clock.h, the arithmetic Scanline Sync on
* Windows uses in place of polling D3DKMTGetScanLine: a vblank clock fed
* the time of each vblank, and the beam position it implies.
*
* A simulated display - a true refresh period a little off its nominal
* rate, as real ones are, and a mode of active/total lines - delivers
* vblanks through a "waiter" that wakes late by random jitter, as the
* clock thread's D3DKMTWaitForVerticalBlankEvent return does. The beam
* the clock reports is checked against the true beam.
*
* settles -> no beam before VBLANK_CLOCK_SETTLE vblanks
* tracks -> across 50 displays up to 600 ppm off nominal,
* beam and wait landing within 6 lines
* missed vblanks -> one in fifty never arriving keeps it usable
* duplicate -> the same vblank reported twice is ignored
* stale -> no beam once vblanks stop for two periods
* variable refresh -> a period that wanders is never reported
* past the target -> wait 0, as the counter loop would find
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "gfx/common/vblank_clock.h"
static unsigned failures = 0;
static void check(bool cond, const char *what)
{
printf(" [%s] %s\n", cond ? "pass" : "FAIL", what);
if (!cond)
failures++;
}
/* 1920x1080p at "60 Hz": 1125 total lines, the true period 0.01% off
* nominal. */
#define ACTIVE 1080u
#define TOTAL 1125u
#define NOMINAL_US (1000000.0 / 60.0)
#define TRUE_US (NOMINAL_US * 1.0001)
static double true_vblank(int n) { return 1000000.0 + n * TRUE_US; }
/* Wake-up lateness: mostly tens of us, sometimes a few hundred. */
static double jitter(void)
{
double r = (double)rand() / RAND_MAX;
return r < 0.95 ? r * 60.0 : 60.0 + r * 400.0;
}
static int line_err(int a, int b)
{
int d = abs(a - b);
return d > (int)TOTAL / 2 ? (int)TOTAL - d : d;
}
/* One simulated run: a true period @ppm off nominal, @frames vblanks
* of which about one in @miss_every never arrives (0: none). Returns the
* worst beam error and the worst wait landing, from the frame after the
* clock settles, in lines. */
static void run(unsigned seed, double ppm, int frames, int miss_every,
int *worst_beam, int *worst_wait, double *period_err, int *first_use)
{
vblank_clock_t c;
double true_us = NOMINAL_US * (1.0 + ppm / 1000000.0);
int n, i;
srand(seed);
vblank_clock_init(&c, NOMINAL_US);
*worst_beam = *worst_wait = 0;
*first_use = -1;
for (n = 0; n < frames; n++)
{
double vb = 1000000.0 + n * true_us;
if (miss_every && rand() % miss_every == 0)
continue;
vblank_clock_feed(&c, (int64_t)(vb + jitter()));
for (i = 1; i < 10; i++)
{
double t = vb + 500.0 + i * (true_us - 1000.0) / 10.0;
int beam = vblank_clock_beam(&c, (int64_t)t, ACTIVE, TOTAL);
int target = 200 + 90 * i;
int64_t w;
double since;
int truth;
if (beam < 0)
continue;
if (*first_use < 0)
*first_use = n;
since = fmod(t - 1000000.0, true_us);
truth = (int)ACTIVE + (int)(since / true_us * TOTAL);
if (truth >= (int)TOTAL)
truth -= (int)TOTAL;
if (line_err(beam, truth) > *worst_beam)
*worst_beam = line_err(beam, truth);
w = vblank_clock_until_line(&c, (int64_t)t, ACTIVE, TOTAL, target);
if (w > 0)
{
double tl = fmod(t + (double)w - 1000000.0, true_us);
int landed = (int)ACTIVE + (int)(tl / true_us * TOTAL);
if (landed >= (int)TOTAL)
landed -= (int)TOTAL;
if (line_err(landed, target) > *worst_wait)
*worst_wait = line_err(landed, target);
}
}
}
*period_err = c.period_us - true_us;
}
int main(void)
{
vblank_clock_t c;
int n, i;
unsigned seed;
int wb = 0, ww = 0, first = 1 << 30, any_early = 0;
double worst_period = 0.0;
printf("tracks: 50 displays, -600..+600 ppm off nominal, waiter up to 460 us late\n");
for (seed = 1; seed <= 50; seed++)
{
int b, w, f;
double pe;
run(seed, (double)(((int)seed % 7) - 3) * 200.0, 1200, 0, &b, &w, &pe, &f);
if (b > wb) wb = b;
if (w > ww) ww = w;
if (fabs(pe) > worst_period) worst_period = fabs(pe);
if (f >= 0 && f < first) first = f;
if (f >= 0 && f < VBLANK_CLOCK_SETTLE) any_early = 1;
}
printf(" (worst beam error %d lines, worst wait landing %d lines, worst period "
"error %.2f us; one D3DKMTGetScanLine call takes ~15 lines)\n", wb, ww, worst_period);
check(!any_early, "no beam before it has settled");
check(wb <= 6, "beam within 6 lines of the truth (~90 us)");
check(ww <= 6, "a wait lands within 6 lines of its target");
check(worst_period < 5.0, "period converges within 5 us");
printf("missed vblanks: one in fifty never reported\n");
{
int b, w, f;
double pe;
run(99, 300.0, 3000, 50, &b, &w, &pe, &f);
check(f >= 0 && b <= 6, "stays usable and on the beam");
}
/* a settled clock for the cases below */
srand(7);
vblank_clock_init(&c, NOMINAL_US);
for (n = 0; n < 400; n++)
vblank_clock_feed(&c, (int64_t)(true_vblank(n) + jitter()));
printf("duplicate\n");
{
vblank_clock_t d = c;
double t = true_vblank(n - 1) + 5000.0;
int before = vblank_clock_beam(&d, (int64_t)t, ACTIVE, TOTAL);
vblank_clock_feed(&d, d.last_us + 20); /* the same vblank again */
check(vblank_clock_beam(&d, (int64_t)t, ACTIVE, TOTAL) == before
&& d.good == c.good, "a vblank reported twice is ignored");
}
printf("stale\n");
{
double t = true_vblank(n - 1) + 2.5 * TRUE_US;
check(vblank_clock_beam(&c, (int64_t)t, ACTIVE, TOTAL) < 0,
"no beam after two periods without a vblank");
}
printf("variable refresh\n");
{
vblank_clock_t v;
double t = 1000000.0;
int reported = 0;
vblank_clock_init(&v, NOMINAL_US);
for (i = 0; i < 2000; i++)
{
/* 48-60 Hz, a new rate every frame */
t += 1000000.0 / (48.0 + 12.0 * rand() / RAND_MAX);
vblank_clock_feed(&v, (int64_t)t);
if (vblank_clock_beam(&v, (int64_t)t + 3000, ACTIVE, TOTAL) >= 0)
reported++;
}
check(reported == 0, "a wandering period is never reported");
}
printf("past the target\n");
{
double t = true_vblank(n - 1) + 0.9 * TRUE_US; /* near the bottom */
check(vblank_clock_until_line(&c, (int64_t)t, ACTIVE, TOTAL, 100) == 0,
"wait is 0 when the beam is already past the line");
}
printf("scanline sampling (XDDM): readings in, vblanks out\n");
{
/* A counter read twice a frame at varying points, each read
* taking 2-30 us, and a driver that reports blanking as an error.
* The period the sampler is given starts at the mode's whole-Hz
* rate, as EnumDisplaySettings reports it, and follows the clock
* once that has settled. */
vblank_clock_t k;
vblank_sampler_t smp;
double true_us = 1000000.0 / 59.94;
unsigned total_seen = 0;
int worst = 0, used = 0;
memset(&smp, 0, sizeof(smp));
vblank_clock_init(&k, 1000000.0 / 60.0);
srand(11);
for (n = 0; n < 1500; n++)
{
int r;
for (r = 0; r < 2; r++)
{
double frac = 0.05 + 0.85 * rand() / (double)RAND_MAX;
double t0 = 1000000.0 + n * true_us + frac * true_us;
double cost = 2.0 + 28.0 * rand() / (double)RAND_MAX;
double at = t0 + cost * rand() / (double)RAND_MAX;
double since = fmod(at - 1000000.0, true_us);
int line = (int)ACTIVE + (int)(since / true_us * TOTAL);
unsigned tot = 0;
int64_t vb;
double period = (k.good >= VBLANK_CLOCK_SETTLE) ? k.period_us : k.nominal_us;
if (line >= (int)TOTAL)
line -= (int)TOTAL;
if (line >= (int)ACTIVE)
line = -1; /* DDERR_VERTICALBLANKINPROGRESS */
vb = vblank_sampler_feed(&smp, (int64_t)(t0 + cost / 2.0),
line, ACTIVE, period, &tot);
if (vb)
{
vblank_clock_feed(&k, vb);
total_seen = tot;
}
}
if (n > 300)
{
double t = 1000000.0 + n * true_us + 0.5 * true_us;
int beam = total_seen ? vblank_clock_beam(&k, (int64_t)t, ACTIVE, total_seen) : -1;
if (beam >= 0)
{
double since = fmod(t - 1000000.0, true_us);
int truth = (int)ACTIVE + (int)(since / true_us * TOTAL);
if (truth >= (int)TOTAL)
truth -= (int)TOTAL;
used++;
if (line_err(beam, truth) > worst)
worst = line_err(beam, truth);
}
}
}
printf(" (total lines implied %u of %u, worst beam error %d lines, usable %d of 1199 frames)\n",
total_seen, TOTAL, worst, used);
check(total_seen >= TOTAL - 3 && total_seen <= TOTAL + 3,
"the total line count comes out of the rate");
check(used > 1100 && worst <= 8, "the beam follows from readings alone, within 8 lines");
}
if (failures)
{
printf("\n%u failure(s)\n", failures);
return 1;
}
printf("\nall passed\n");
return 0;
}
+29
View File
@@ -0,0 +1,29 @@
TARGET := vblank_clock_ddraw_test.exe
RARCH_DIR := ../../..
LIBRETRO_COMM_DIR := $(RARCH_DIR)/libretro-common
# A Windows program: built with MinGW, run on Windows or under wine
# (with a display - xvfb-run on a headless box).
MINGW_CC := $(shell command -v x86_64-w64-mingw32-gcc 2>/dev/null)
WINE := $(shell command -v wine 2>/dev/null)
CFLAGS += -std=gnu99 -Wall -O1 -I$(RARCH_DIR) -I$(LIBRETRO_COMM_DIR)/include
ifeq ($(MINGW_CC),)
all check:
@echo "vblank_clock_ddraw_test: no x86_64-w64-mingw32-gcc, skipped"
else
all: $(TARGET)
$(TARGET): vblank_clock_ddraw_test.c $(RARCH_DIR)/gfx/common/vblank_clock.h
$(MINGW_CC) $(CFLAGS) -o $@ vblank_clock_ddraw_test.c -lm
check: all
$(WINE) ./$(TARGET)
endif
clean:
rm -f $(TARGET)
.PHONY: all check clean
@@ -0,0 +1,96 @@
/* Scanline Sync's XDDM tier (Windows 2000 and XP) against a real
* DirectDraw: IDirectDraw::GetScanLine readings, taken at varying points
* of each frame as the frame loop takes them, through
* vblank_sampler_feed() into vblank_clock - the same two calls
* gfx/common/win32_common.c makes - and the clock's beam then checked
* against fresh GetScanLine reads.
*
* Built with MinGW; run under wine, whose DirectDraw emulates the
* counter the way an XDDM driver reports it (lines in the active region,
* DDERR_VERTICALBLANKINPROGRESS in blanking), or on Windows.
*
* the clock settles from readings alone
* its beam matches GetScanLine within 10 lines */
#include <windows.h>
#include <ddraw.h>
#include <stdio.h>
#include <stdint.h>
#include "gfx/common/vblank_clock.h"
static int64_t now_us(void)
{
static LARGE_INTEGER f; LARGE_INTEGER c;
if (!f.QuadPart) QueryPerformanceFrequency(&f);
QueryPerformanceCounter(&c);
return (int64_t)(c.QuadPart / f.QuadPart * 1000000 + c.QuadPart % f.QuadPart * 1000000 / f.QuadPart);
}
int main(void)
{
typedef HRESULT (WINAPI *create_fn)(GUID*, LPDIRECTDRAW*, IUnknown*);
HMODULE dll = LoadLibraryA("ddraw.dll");
create_fn create = dll ? (create_fn)GetProcAddress(dll, "DirectDrawCreate") : NULL;
LPDIRECTDRAW dd = NULL;
DEVMODEA dm;
vblank_clock_t vc;
vblank_sampler_t smp;
unsigned active, total = 0;
double nominal;
int i, frame, errs = 0, worst = 0, checks = 0, blanks = 0, lines = 0;
DWORD maxline = 0;
memset(&dm, 0, sizeof(dm)); dm.dmSize = sizeof(dm);
EnumDisplaySettingsA(NULL, ENUM_CURRENT_SETTINGS, &dm);
active = dm.dmPelsHeight;
nominal = 1000000.0 / (dm.dmDisplayFrequency > 1 ? dm.dmDisplayFrequency : 60);
printf("mode %lux%lu @ %lu Hz\n", dm.dmPelsWidth, dm.dmPelsHeight, dm.dmDisplayFrequency);
if (!create || FAILED(create(NULL, &dd, NULL))) { printf("no DirectDraw\n"); return 2; }
/* what wine's counter looks like */
for (i = 0; i < 20000; i++)
{
DWORD l = 0; HRESULT hr = dd->lpVtbl->GetScanLine(dd, &l);
if (hr == DD_OK) { lines++; if (l > maxline) maxline = l; }
else if (hr == DDERR_VERTICALBLANKINPROGRESS) blanks++;
else errs++;
}
printf("counter: %d lines (max %lu), %d in-blank, %d errors\n", lines, maxline, blanks, errs);
memset(&smp, 0, sizeof(smp));
vblank_clock_init(&vc, nominal);
for (frame = 0; frame < 400; frame++)
{
int r;
for (r = 0; r < 2; r++)
{
DWORD l = 0; int64_t t0, t1, vb; HRESULT hr; unsigned tot = 0;
/* readings at varying points in the frame, as a frame loop
* would take them - a Sleep stands in for the core running */
Sleep(1 + (frame * 7 + r * 5) % 6);
t0 = now_us(); hr = dd->lpVtbl->GetScanLine(dd, &l); t1 = now_us();
vb = vblank_sampler_feed(&smp, t0 + (t1 - t0) / 2,
hr == DD_OK ? (int)l : -1, active,
vc.good >= VBLANK_CLOCK_SETTLE ? vc.period_us : nominal, &tot);
if (vb) { total = tot; vblank_clock_feed(&vc, vb); }
}
if (frame > 200)
{
DWORD l = 0; int64_t t0 = now_us();
HRESULT hr = dd->lpVtbl->GetScanLine(dd, &l);
int64_t t1 = now_us();
int beam = total ? vblank_clock_beam(&vc, t0 + (t1 - t0) / 2, active, total) : -1;
if (hr == DD_OK && beam >= 0 && beam < (int)active)
{
int d = abs((int)l - beam);
if (d > (int)total / 2) d = (int)total - d;
if (d > worst) worst = d;
checks++;
}
}
}
printf("sampler: rate %.5f lines/us from %u pairs, total lines %u\n", smp.rate, smp.rate_n, total);
printf("clock: settled %s, period %.1f us (nominal %.1f)\n", vc.good >= VBLANK_CLOCK_SETTLE ? "yes" : "no", vc.period_us, nominal);
printf("beam against GetScanLine: worst %d lines over %d checks\n", worst, checks);
dd->lpVtbl->Release(dd);
return (vc.good >= VBLANK_CLOCK_SETTLE && checks > 100 && worst <= 10) ? 0 : 1;
}