Files
spice2x.github.io/src/spice2x/hooks/audio/backends/wasapi/shared.h
T
bicarus 4af006e869 wasapi: address buffer overflow when using -wasapishared (#758)
## Link to GitHub Issue or related Pull Request, if one exists
Building on #745 

## Description of change
With `-wasapishared`, some users see `AUDCLNT_E_BUFFER_TOO_LARGE`
(`0x88890006`).

The existing one-device-period buffer clamp wasn't enough on endpoints
with a small shared buffer; the shared buffer is double-buffered, so a
full-buffer write often exceeds the free space.

### Fix

Added a FIFO bridge to `SharedRedirect` that decouples the game's
per-event writes from the shared engine's clock:

- **`GetBuffer`** hands the game a pointer into the FIFO tail to write
in place.
- **`ReleaseBuffer`** commits the write and drains `min(pending,
device_free)` frames to the device - so a write can never exceed what
the device accepts, structurally preventing the overflow on any
endpoint.
- **`GetCurrentPadding`** reports the FIFO fill level (capped to the
reported buffer size) so poll/timer-driven games pace correctly against
the virtual buffer.

## Testing
2026-06-14 19:57:28 -07:00

84 lines
4.3 KiB
C++

#pragma once
#include <cstdint>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
struct IAudioRenderClient;
namespace hooks::audio {
// The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps
// can play sound and devices that can't open the exclusive format still work, at the cost of
// some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone.
struct SharedRedirect {
// true once apply() has redirected an exclusive request. gates the buffer clamp; stays false
// for a natively-shared stream (it paces itself, so must not be clamped).
bool redirected_from_exclusive = false;
// whether an exclusive-mode request should be redirected, given the -wasapishared option.
// only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode.
static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format);
// redirect an exclusive request to shared mode. caller must have checked wants() first.
void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity);
// clamp a reported buffer size to one device period. the FIFO bridge below is what prevents
// the overflow; this just keeps the game's per-event writes small so the bridge adds minimal
// latency. caches the chosen value for virtual_padding. a no-op unless redirected.
UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames);
// FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own
// callback, not the shared engine clock, so a full-buffer write can intermittently exceed the
// double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes
// directly into a FIFO that is drained to the device only as fast as it frees space - the
// same free-space-clamped approach the timer-driven resampler uses.
// arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size
// in the game's (== device, via AUTOCONVERTPCM) format.
void enable_bridge(int frame_bytes);
// whether the FIFO bridge is active (a redirect was applied and armed).
bool bridge_enabled() const { return this->frame_bytes > 0; }
// reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place.
// must be paired with commit_write, which trims the reservation to the frames written.
BYTE *begin_write(UINT32 frames);
// trim the reservation from begin_write to the `frames` actually written (zeroing if silent).
void commit_write(UINT32 frames, bool silent);
// padding to report to a game that polls GetCurrentPadding while the bridge is active: the
// FIFO fill level, capped to the reported buffer size so the game's free-space calculation
// (reported_buffer - padding) reflects room in the virtual buffer rather than the device's.
UINT32 virtual_padding() const;
// push as many queued frames as the real device has free, applying `boost`, keeping the rest
// for the next call. `real` is the wrapped render client's underlying interface; `client` is
// the underlying audio client used to query the device's free space.
HRESULT drain(IAudioRenderClient *real, IAudioClient *client,
const WAVEFORMATEXTENSIBLE &device_format, float boost);
private:
// frames currently queued in the FIFO and not yet handed to the device.
UINT32 pending_frames() const;
// FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the
// game's interleaved frame format; the game writes new frames directly into its tail between
// begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see
// bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write.
int frame_bytes = 0;
UINT32 device_buffer_frames = 0;
UINT32 reported_frames = 0;
int buffers_to_mute = 4;
size_t pending_write_offset = 0;
std::vector<BYTE> fifo;
};
}