wasapi: initialise the shared client at the engine's minimum period via IAudioClient3

Shared mode passed hnsPeriodicity 0 to IAudioClient::Initialize, which
means the engine's default period - 10 ms on a typical desktop. The
write event fires once per engine period, so a client blocking on it
is released on a 10 ms grid regardless of buffer size. With audio_sync
on that is the grid the whole frame loop is released on, and at 60 fps
it is the largest single source of frame-time jitter under VRR.

Windows 10 1607 added IAudioClient3::InitializeSharedAudioStream,
which lets a shared client run at a smaller engine period, down to the
minimum GetSharedModeEnginePeriod reports - typically 128 frames,
2.67 ms at 48 kHz. Use it when it is there: query the interface, read
the minimum, initialise at it. The event now fires every ~2.7 ms and
the release grid is four times finer.

Backwards compatibility, at each layer:

  Build. Everything new sits under __IAudioClient3_INTERFACE_DEFINED__,
  the guard the SDK header sets. An SDK without the interface compiles
  the IAudioClient path only, byte for byte as before. The IID is
  defined locally rather than through the SDK export, which mingw's
  libuuid lacks.

  Runtime. QueryInterface fails on pre-1607 Windows, or the period
  query or the initialise can fail on a driver that does not support
  it; in every case the client is released, re-activated fresh, and
  handed to the unchanged legacy path. Nothing downstream can tell the
  difference except the flag.

  Latency budget. The default FIFO sizing takes the engine buffer
  size, and under IAudioClient3 the engine sizes that buffer to the
  small period - a few ms, no longer reflecting audio_latency. When the
  low-latency path engaged, the FIFO is sized from audio_latency
  directly, so the user's buffer is what it always was and only the
  drain cadence changed. Exclusive mode and the microphone are
  untouched; the microphone passes NULL for the new out-parameter.

Both COM macro branches (C lpVtbl and C++) carry the new methods.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 07:23:53 +02:00
1 parent 99f381f4ac
commit 338abdf045
2 files changed
+109 -8

No files matched your search

+26
View File
@@ -116,6 +116,16 @@ DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80, 0
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); /* DEVPROP_TYPE_STRING */
/* IAudioClient3 (Windows 10 1607+): shared-mode streams at an engine
* period smaller than the default. Only compiled where the SDK declares
* the interface; older SDKs take the IAudioClient path unchanged. The
* IID is defined here under our own name rather than relying on the
* SDK's IID_IAudioClient3 export, which mingw's libuuid does not carry. */
#ifdef __IAudioClient3_INTERFACE_DEFINED__
static const GUID mmdevice_IID_IAudioClient3 =
{ 0x7ed4ee07, 0x8e67, 0x4cd4, { 0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42 } };
#endif
#ifdef __cplusplus
#define _IMMDeviceCollection_Item(This,nDevice,ppdevice) (This)->Item(nDevice,ppdevice)
#define _IAudioClient_Start(This) ( (This)->Start() )
@@ -133,6 +143,14 @@ DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0
#define _IAudioClient_GetDevicePeriod(This,phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) ( (This)->GetDevicePeriod(phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) )
#define _IAudioClient_Initialize(This,ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid) \
( (This)->Initialize(ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid))
#define _IAudioClient_QueryInterface(This,riid,ppv) ( (This)->QueryInterface(riid,ppv) )
#ifdef __IAudioClient3_INTERFACE_DEFINED__
#define _IAudioClient3_GetSharedModeEnginePeriod(This,pFormat,pDefault,pFundamental,pMin,pMax) \
( (This)->GetSharedModeEnginePeriod(pFormat,pDefault,pFundamental,pMin,pMax) )
#define _IAudioClient3_InitializeSharedAudioStream(This,StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid) \
( (This)->InitializeSharedAudioStream(StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid) )
#define _IAudioClient3_Release(This) ( (This)->Release() )
#endif
#define _IAudioClient_IsFormatSupported(This,ShareMode,pFormat,ppClosestMatch) \
( (This)->IsFormatSupported(ShareMode,pFormat,ppClosestMatch))
#define _IMMDevice_Activate(This,iid,dwClsCtx,pActivationParams,ppv) ((This)->Activate(iid,(dwClsCtx),pActivationParams,ppv))
@@ -167,6 +185,14 @@ DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0
#define _IAudioClient_GetDevicePeriod(This,phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) ( (This)->lpVtbl -> GetDevicePeriod(This,phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) )
#define _IAudioClient_Initialize(This,ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid) \
( (This)->lpVtbl->Initialize(This,ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid))
#define _IAudioClient_QueryInterface(This,riid,ppv) ( (This)->lpVtbl->QueryInterface(This,riid,ppv) )
#ifdef __IAudioClient3_INTERFACE_DEFINED__
#define _IAudioClient3_GetSharedModeEnginePeriod(This,pFormat,pDefault,pFundamental,pMin,pMax) \
( (This)->lpVtbl->GetSharedModeEnginePeriod(This,pFormat,pDefault,pFundamental,pMin,pMax) )
#define _IAudioClient3_InitializeSharedAudioStream(This,StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid) \
( (This)->lpVtbl->InitializeSharedAudioStream(This,StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid))
#define _IAudioClient3_Release(This) ( (This)->lpVtbl->Release(This) )
#endif
#define _IAudioClient_IsFormatSupported(This,ShareMode,pFormat,ppClosestMatch) \
( (This)->lpVtbl->IsFormatSupported(This,ShareMode,pFormat,ppClosestMatch))
#define _IMMDevice_Activate(This,iid,dwClsCtx,pActivationParams,ppv) ((This)->lpVtbl->Activate(This,&(iid),dwClsCtx,pActivationParams,ppv))
+83 -8
View File
@@ -57,7 +57,11 @@ enum wasapi_flags
{
WASAPI_FLG_EXCLUSIVE = (1 << 0),
WASAPI_FLG_NONBLOCK = (1 << 1),
WASAPI_FLG_RUNNING = (1 << 2)
WASAPI_FLG_RUNNING = (1 << 2),
/* Shared client initialised via IAudioClient3 at the engine's minimum
* period rather than its default. The client FIFO keeps its
* audio_latency size; only the release cadence is finer. */
WASAPI_FLG_LOWLAT = (1 << 3)
};
typedef struct
@@ -430,7 +434,8 @@ error:
}
static IAudioClient *wasapi_init_client_sh(IMMDevice *device,
bool *float_fmt, unsigned *rate, unsigned latency, unsigned channels)
bool *float_fmt, unsigned *rate, unsigned latency, unsigned channels,
bool *low_latency)
{
WAVEFORMATEXTENSIBLE wf;
IAudioClient *client = NULL;
@@ -477,6 +482,60 @@ static IAudioClient *wasapi_init_client_sh(IMMDevice *device,
goto error;
}
if (low_latency)
*low_latency = false;
#ifdef __IAudioClient3_INTERFACE_DEFINED__
/* Windows 10 1607+: ask the engine for its minimum shared-mode
* period instead of the default 10 ms. The write event then fires
* at that period, and a client blocking on it is released on a
* grid several times finer - which, with audio_sync on, is the grid
* the whole frame loop is released on. Any failure here falls
* through to the IAudioClient path below on a fresh client, so a
* system that cannot do this behaves exactly as before. */
{
IAudioClient3 *client3 = NULL;
hr = _IAudioClient_QueryInterface(client,
&mmdevice_IID_IAudioClient3, (void**)&client3);
if (SUCCEEDED(hr) && client3)
{
UINT32 p_default = 0, p_fundamental = 0, p_min = 0, p_max = 0;
hr = _IAudioClient3_GetSharedModeEnginePeriod(client3,
(WAVEFORMATEX*)&wf, &p_default, &p_fundamental, &p_min, &p_max);
if (SUCCEEDED(hr) && p_min > 0 && p_min < p_default)
{
hr = _IAudioClient3_InitializeSharedAudioStream(client3,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK, p_min,
(WAVEFORMATEX*)&wf, NULL);
if (SUCCEEDED(hr))
{
if (low_latency)
*low_latency = true;
RARCH_LOG("[WASAPI] Shared client at engine period %u frames (%.2f ms; default %u).\n",
p_min, (double)p_min * 1000.0 / (double)wf.Format.nSamplesPerSec,
p_default);
}
}
_IAudioClient3_Release(client3);
}
if (low_latency && *low_latency)
goto initialized;
/* Not taken, or failed partway: start again from a fresh client
* so the legacy path sees exactly the object it always has. */
RELEASE(client);
hr = _IMMDevice_Activate(device, IID_IAudioClient, CLSCTX_ALL, NULL,
(void**)&client);
if (FAILED(hr))
{
RARCH_ERR("[WASAPI] IMMDevice::Activate failed: %s.\n",
mmdevice_hresult_name(hr));
return NULL;
}
}
#endif
hr = _IAudioClient_Initialize(client, AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
buffer_duration, 0, (WAVEFORMATEX*)&wf, NULL);
@@ -507,6 +566,9 @@ static IAudioClient *wasapi_init_client_sh(IMMDevice *device,
goto error;
}
#ifdef __IAudioClient3_INTERFACE_DEFINED__
initialized:
#endif
*float_fmt = wf.Format.wFormatTag != WAVE_FORMAT_PCM;
*rate = wf.Format.nSamplesPerSec;
return client;
@@ -518,7 +580,8 @@ error:
static IAudioClient *wasapi_init_client(IMMDevice *device, bool *exclusive,
bool *float_fmt, unsigned *rate, unsigned latency, unsigned channels)
bool *float_fmt, unsigned *rate, unsigned latency, unsigned channels,
bool *low_latency)
{
HRESULT hr;
IAudioClient *client;
@@ -534,14 +597,14 @@ static IAudioClient *wasapi_init_client(IMMDevice *device, bool *exclusive,
if (!client)
{
RARCH_WARN("[WASAPI] Failed to initialize exclusive client, attempting shared client.\n");
client = wasapi_init_client_sh(device, float_fmt, rate, latency, channels);
client = wasapi_init_client_sh(device, float_fmt, rate, latency, channels, low_latency);
if (client)
*exclusive = false;
}
}
else
{
client = wasapi_init_client_sh(device, float_fmt, rate, latency, channels);
client = wasapi_init_client_sh(device, float_fmt, rate, latency, channels, low_latency);
if (!client)
{
RARCH_WARN("[WASAPI] Failed to initialize shared client, attempting exclusive client.\n");
@@ -954,7 +1017,7 @@ static void *wasapi_microphone_open_mic(void *driver_context, const char *device
}
mic->client = wasapi_init_client(mic->device,
&mic->exclusive, &float_format, &rate, latency, 1);
&mic->exclusive, &float_format, &rate, latency, 1, NULL);
if (!mic->client)
{
RARCH_ERR("[WASAPI] Failed to open client for capture device \"%s\".\n", mic->device_name);
@@ -1160,6 +1223,7 @@ static void *wasapi_init(const char *dev_id, unsigned rate, unsigned latency,
bool exclusive_mode = settings->bools.audio_wasapi_exclusive_mode;
bool audio_sync = settings->bools.audio_sync;
unsigned sh_buffer_length = settings->uints.audio_wasapi_sh_buffer_length;
bool low_latency = false;
wasapi_t *w = (wasapi_t*)calloc(1, sizeof(wasapi_t));
if (!w)
@@ -1172,10 +1236,12 @@ static void *wasapi_init(const char *dev_id, unsigned rate, unsigned latency,
goto error;
if (!(w->client = wasapi_init_client(w->device,
&exclusive_mode, &float_format, &rate, latency, 2)))
&exclusive_mode, &float_format, &rate, latency, 2, &low_latency)))
goto error;
if (exclusive_mode)
w->flags |= WASAPI_FLG_EXCLUSIVE;
if (low_latency)
w->flags |= WASAPI_FLG_LOWLAT;
hr = _IAudioClient_GetBufferSize(w->client, &frame_count);
if (FAILED(hr))
@@ -1196,7 +1262,16 @@ static void *wasapi_init(const char *dev_id, unsigned rate, unsigned latency,
{
case WASAPI_SH_BUFFER_AUDIO_LATENCY:
case WASAPI_SH_BUFFER_CLIENT_BUFFER:
sh_buffer_length = frame_count;
/* Under IAudioClient3 the engine sizes its own buffer to the
* small period, so frame_count is a few ms and no longer
* reflects audio_latency. Keep the FIFO at the latency the
* user asked for; the engine buffer is only the drain step. */
if (w->flags & WASAPI_FLG_LOWLAT)
sh_buffer_length = (unsigned)(((uint64_t)latency * rate) / 1000);
else
sh_buffer_length = frame_count;
if (sh_buffer_length < frame_count)
sh_buffer_length = frame_count;
break;
case WASAPI_SH_BUFFER_DEVICE_PERIOD:
hr = _IAudioClient_GetDevicePeriod(w->client, &dev_period, NULL);