mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4be745e5 | ||
|
|
2fbd665566 | ||
|
|
9dd810c96e | ||
|
|
8d61347d46 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user