From 7c24b585af0d7f94cc697e0d4134989ffdf6e9e2 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 1 Sep 2026 22:00:00 +1000 Subject: [PATCH] [idd] audio: add virtual playback and capture driver Add a WaveRT adapter with render and capture endpoints backed by a local ring buffer and shared presentation clock. Advertise PCM formats through 192 kHz, eight channels, and packed 24-bit samples. Register LGAudio in the IDD solution and package it as Looking Glass Audio. --- idd/LGAudio/Adapter.cpp | 253 +++++++ idd/LGAudio/Adapter.h | 76 ++ idd/LGAudio/AudioClock.cpp | 57 ++ idd/LGAudio/AudioClock.h | 70 ++ idd/LGAudio/Common.h | 79 ++ idd/LGAudio/Driver.cpp | 99 +++ idd/LGAudio/Formats.cpp | 455 ++++++++++++ idd/LGAudio/Formats.h | 125 ++++ idd/LGAudio/Guids.cpp | 22 + idd/LGAudio/Guids.h | 23 + idd/LGAudio/LGAudio.inx | 125 ++++ idd/LGAudio/LGAudio.rc | 31 + idd/LGAudio/LGAudio.vcxproj | 104 +++ idd/LGAudio/LGAudio.vcxproj.filters | 46 ++ idd/LGAudio/MiniportTopology.cpp | 231 ++++++ idd/LGAudio/MiniportTopology.h | 56 ++ idd/LGAudio/MiniportWaveRT.cpp | 326 ++++++++ idd/LGAudio/MiniportWaveRT.h | 71 ++ idd/LGAudio/MiniportWaveRTStream.cpp | 1026 ++++++++++++++++++++++++++ idd/LGAudio/MiniportWaveRTStream.h | 163 ++++ idd/LGAudio/RingBuffer.cpp | 323 ++++++++ idd/LGAudio/RingBuffer.h | 103 +++ idd/LGIdd.sln | 12 + 23 files changed, 3876 insertions(+) create mode 100644 idd/LGAudio/Adapter.cpp create mode 100644 idd/LGAudio/Adapter.h create mode 100644 idd/LGAudio/AudioClock.cpp create mode 100644 idd/LGAudio/AudioClock.h create mode 100644 idd/LGAudio/Common.h create mode 100644 idd/LGAudio/Driver.cpp create mode 100644 idd/LGAudio/Formats.cpp create mode 100644 idd/LGAudio/Formats.h create mode 100644 idd/LGAudio/Guids.cpp create mode 100644 idd/LGAudio/Guids.h create mode 100644 idd/LGAudio/LGAudio.inx create mode 100644 idd/LGAudio/LGAudio.rc create mode 100644 idd/LGAudio/LGAudio.vcxproj create mode 100644 idd/LGAudio/LGAudio.vcxproj.filters create mode 100644 idd/LGAudio/MiniportTopology.cpp create mode 100644 idd/LGAudio/MiniportTopology.h create mode 100644 idd/LGAudio/MiniportWaveRT.cpp create mode 100644 idd/LGAudio/MiniportWaveRT.h create mode 100644 idd/LGAudio/MiniportWaveRTStream.cpp create mode 100644 idd/LGAudio/MiniportWaveRTStream.h create mode 100644 idd/LGAudio/RingBuffer.cpp create mode 100644 idd/LGAudio/RingBuffer.h diff --git a/idd/LGAudio/Adapter.cpp b/idd/LGAudio/Adapter.cpp new file mode 100644 index 00000000..6ccd20ca --- /dev/null +++ b/idd/LGAudio/Adapter.cpp @@ -0,0 +1,253 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "Adapter.h" +#include "Formats.h" +#include "MiniportTopology.h" +#include "MiniportWaveRT.h" + +namespace +{ +NTSTATUS InstallTopologySubdevice( + _In_ PDEVICE_OBJECT deviceObject, + _In_ PIRP irp, + _In_ PRESOURCELIST resourceList, + _In_ CAudioAdapter *adapter, + _In_ EndpointKind kind, + _In_ const wchar_t *name, + _Outptr_ PUNKNOWN *portUnknown) +{ + *portUnknown = nullptr; + + PPORT port = nullptr; + NTSTATUS status = PcNewPort(&port, CLSID_PortTopology); + if (!NT_SUCCESS(status)) + return status; + + CMiniportTopology *miniport = new(NonPagedPoolNx, LGAUDIO_POOL_TAG) + CMiniportTopology(kind); + if (!miniport) + { + port->Release(); + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = port->Init(deviceObject, irp, + static_cast(miniport), adapter, resourceList); + if (NT_SUCCESS(status)) + status = PcRegisterSubdevice(deviceObject, + const_cast(name), port); + + miniport->Release(); + + if (!NT_SUCCESS(status)) + { + port->Release(); + return status; + } + + *portUnknown = port; + return STATUS_SUCCESS; +} + +NTSTATUS InstallWaveSubdevice( + _In_ PDEVICE_OBJECT deviceObject, + _In_ PIRP irp, + _In_ PRESOURCELIST resourceList, + _In_ CAudioAdapter *adapter, + _In_ EndpointKind kind, + _In_ const wchar_t *name, + _Outptr_ PUNKNOWN *portUnknown) +{ + *portUnknown = nullptr; + + PPORT port = nullptr; + NTSTATUS status = PcNewPort(&port, CLSID_PortWaveRT); + if (!NT_SUCCESS(status)) + return status; + + CMiniportWaveRT *miniport = new(NonPagedPoolNx, LGAUDIO_POOL_TAG) + CMiniportWaveRT(adapter, kind); + if (!miniport) + { + port->Release(); + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = port->Init(deviceObject, irp, + static_cast(miniport), adapter, resourceList); + if (NT_SUCCESS(status)) + status = PcRegisterSubdevice(deviceObject, + const_cast(name), port); + + miniport->Release(); + + if (!NT_SUCCESS(status)) + { + port->Release(); + return status; + } + + *portUnknown = port; + return STATUS_SUCCESS; +} +} + +CAudioAdapter::CAudioAdapter() : + m_refCount(1) +{ +} + +CAudioAdapter::~CAudioAdapter() +{ + m_ring.Destroy(); +} + +NTSTATUS CAudioAdapter::Initialize() +{ + NTSTATUS status = m_clock.Initialize(); + if (NT_SUCCESS(status)) + status = m_ring.Init(); + return status; +} +_IRQL_requires_max_(DISPATCH_LEVEL) +NTSTATUS CAudioAdapter::ConfigureRender( + _In_ const FormatFingerprint &format) +{ + return m_ring.Configure(format); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +void CAudioAdapter::ResetCapture() +{ + m_ring.Reset(); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +void CAudioAdapter::WriteRenderFrames( + _In_reads_bytes_(bytes) const BYTE *data, + _In_ ULONG bytes) +{ + m_ring.Push(data, bytes); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +void CAudioAdapter::ReadCaptureFrames( + _In_ const FormatFingerprint &format, + _Out_writes_bytes_(bytes) BYTE *data, + _In_ ULONG bytes) +{ + m_ring.Pop(format, data, bytes); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +void CAudioAdapter::DiscardCaptureFrames( + _In_ const FormatFingerprint &format, + _In_ ULONGLONG bytes) +{ + m_ring.Discard(format, bytes); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +AudioClockSnapshot CAudioAdapter::GetClockSnapshot() const +{ + return m_clock.GetSnapshot(); +} + +NTSTATUS CAudioAdapter::QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) +{ + if (!object) + return STATUS_INVALID_PARAMETER; + + *object = nullptr; + if (!GuidEqual(interfaceId, IID_IUnknown)) + return STATUS_NOINTERFACE; + + *object = static_cast(this); + AddRef(); + return STATUS_SUCCESS; +} + +ULONG CAudioAdapter::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refCount)); +} + +ULONG CAudioAdapter::Release() +{ + const LONG count = InterlockedDecrement(&m_refCount); + if (!count) + delete this; + return static_cast(count); +} + +NTSTATUS StartAudioDevice( + _In_ PDEVICE_OBJECT deviceObject, + _In_ PIRP irp, + _In_ PRESOURCELIST resourceList) +{ + PUNKNOWN topologyRender = nullptr; + PUNKNOWN waveRender = nullptr; + PUNKNOWN topologyCapture = nullptr; + PUNKNOWN waveCapture = nullptr; + + CAudioAdapter *adapter = new(NonPagedPoolNx, LGAUDIO_POOL_TAG) + CAudioAdapter(); + if (!adapter) + return STATUS_INSUFFICIENT_RESOURCES; + + NTSTATUS status = adapter->Initialize(); + if (NT_SUCCESS(status)) + status = InstallTopologySubdevice(deviceObject, irp, resourceList, + adapter, EndpointKind::Render, kTopologyRenderName, &topologyRender); + if (NT_SUCCESS(status)) + status = InstallWaveSubdevice(deviceObject, irp, resourceList, + adapter, EndpointKind::Render, kWaveRenderName, &waveRender); + if (NT_SUCCESS(status)) + status = InstallTopologySubdevice(deviceObject, irp, resourceList, + adapter, EndpointKind::Capture, kTopologyCaptureName, + &topologyCapture); + if (NT_SUCCESS(status)) + status = InstallWaveSubdevice(deviceObject, irp, resourceList, + adapter, EndpointKind::Capture, kWaveCaptureName, &waveCapture); + + if (NT_SUCCESS(status)) + status = PcRegisterPhysicalConnection(deviceObject, waveRender, + kRenderWaveBridgePin, topologyRender, kRenderTopologyBridgePin); + if (NT_SUCCESS(status)) + status = PcRegisterPhysicalConnection(deviceObject, topologyCapture, + kCaptureTopologyBridgePin, waveCapture, kCaptureWaveBridgePin); + + SafeRelease(waveCapture); + SafeRelease(topologyCapture); + SafeRelease(waveRender); + SafeRelease(topologyRender); + adapter->Release(); + + if (!NT_SUCCESS(status)) + LGAUDIO_TRACE_ERROR("device start failed: 0x%08X", status); + else + LGAUDIO_TRACE_INFO("render and capture endpoints registered (%s)", + "Looking Glass Audio"); + + return status; +} diff --git a/idd/LGAudio/Adapter.h b/idd/LGAudio/Adapter.h new file mode 100644 index 00000000..7174ff34 --- /dev/null +++ b/idd/LGAudio/Adapter.h @@ -0,0 +1,76 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "AudioClock.h" +#include "Common.h" +#include "RingBuffer.h" + +class CAudioAdapter final : public IUnknown +{ +public: + CAudioAdapter(); + ~CAudioAdapter(); + + NTSTATUS Initialize(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS ConfigureRender( + _In_ const FormatFingerprint &format); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void ResetCapture(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void WriteRenderFrames( + _In_reads_bytes_(bytes) const BYTE *data, + _In_ ULONG bytes); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void ReadCaptureFrames( + _In_ const FormatFingerprint &format, + _Out_writes_bytes_(bytes) BYTE *data, + _In_ ULONG bytes); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void DiscardCaptureFrames( + _In_ const FormatFingerprint &format, + _In_ ULONGLONG bytes); + + _IRQL_requires_max_(DISPATCH_LEVEL) + AudioClockSnapshot GetClockSnapshot() const; + + STDMETHODIMP_(NTSTATUS) QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + +private: + volatile LONG m_refCount; + CAudioClock m_clock; + CAudioRingBuffer m_ring; +}; + +NTSTATUS StartAudioDevice( + _In_ PDEVICE_OBJECT deviceObject, + _In_ PIRP irp, + _In_ PRESOURCELIST resourceList); diff --git a/idd/LGAudio/AudioClock.cpp b/idd/LGAudio/AudioClock.cpp new file mode 100644 index 00000000..99aaf20d --- /dev/null +++ b/idd/LGAudio/AudioClock.cpp @@ -0,0 +1,57 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "AudioClock.h" + +namespace LGAudio +{ + CAudioClock::CAudioClock() : + m_frequency(0) + { + } + + _IRQL_requires_max_(PASSIVE_LEVEL) + NTSTATUS CAudioClock::Initialize() + { + LARGE_INTEGER frequency{}; + KeQueryPerformanceCounter(&frequency); + if (frequency.QuadPart <= 0) + return STATUS_DEVICE_HARDWARE_ERROR; + + m_frequency = static_cast(frequency.QuadPart); + return STATUS_SUCCESS; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + AudioClockSnapshot CAudioClock::GetSnapshot() const + { + const LARGE_INTEGER qpc = KeQueryPerformanceCounter(nullptr); + const ULONGLONG position = static_cast(qpc.QuadPart); + + // The fallback domains are identical, making the correlation exact. + return + { + position, + m_frequency, + position, + m_frequency + }; + } +} diff --git a/idd/LGAudio/AudioClock.h b/idd/LGAudio/AudioClock.h new file mode 100644 index 00000000..4c0c65ae --- /dev/null +++ b/idd/LGAudio/AudioClock.h @@ -0,0 +1,70 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "Common.h" + +namespace LGAudio +{ + /** + * A correlated sample of the audio time domain and the Windows QPC time + * domain. Position drives audio progress; QpcPosition timestamps that + * position for WaveRT clients. + */ + struct AudioClockSnapshot + { + ULONGLONG Position; + ULONGLONG Frequency; + ULONGLONG QpcPosition; + ULONGLONG QpcFrequency; + }; + + /** + * Adapter-owned clock shared by all render and capture streams. + * + * The local fallback uses QPC for both domains. A transport-disciplined + * implementation can later synthesize a continuous, monotonic Position + * while preserving its correlation with the local QPC fields. Before a + * non-unity clock rate is enabled, the stream deadline and timestamp + * conversions must consume an explicit fixed-point affine mapping between + * the two domains. + */ + class CAudioClock final + { + public: + CAudioClock(); + + CAudioClock(const CAudioClock &) = delete; + CAudioClock & operator=(const CAudioClock &) = delete; + + _IRQL_requires_max_(PASSIVE_LEVEL) + NTSTATUS Initialize(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + AudioClockSnapshot GetSnapshot() const; + + private: + ULONGLONG m_frequency; + }; +} + +using LGAudio::AudioClockSnapshot; +using LGAudio::CAudioClock; diff --git a/idd/LGAudio/Common.h b/idd/LGAudio/Common.h new file mode 100644 index 00000000..74efa9c0 --- /dev/null +++ b/idd/LGAudio/Common.h @@ -0,0 +1,79 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +extern "C" +{ +#include +#include +#include +#include +} + +#define LGAUDIO_POOL_TAG 'uAGL' +#define LGAUDIO_RING_TAG 'rAGL' + +#define LGAUDIO_TRACE_ERROR(format, ...) \ + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, \ + "LGAudio: " format "\n", __VA_ARGS__)) + +#define LGAUDIO_TRACE_INFO(format, ...) \ + KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, \ + "LGAudio: " format "\n", __VA_ARGS__)) + +void * __cdecl operator new(size_t size, POOL_TYPE poolType, ULONG tag); +void * __cdecl operator new(size_t size, POOL_FLAGS poolFlags, ULONG tag); +void __cdecl operator delete(void *memory) noexcept; +void __cdecl operator delete(void *memory, size_t size) noexcept; +void __cdecl operator delete(void *memory, POOL_TYPE poolType, + ULONG tag) noexcept; +void __cdecl operator delete(void *memory, POOL_FLAGS poolFlags, + ULONG tag) noexcept; + +template +inline void SafeRelease(T *&object) +{ + if (object) + { + object->Release(); + object = nullptr; + } +} + +inline bool GuidEqual(REFGUID left, REFGUID right) +{ + return !!IsEqualGUIDAligned(left, right); +} + +constexpr ULONG kRenderWaveHostPin = 0; +constexpr ULONG kRenderWaveBridgePin = 1; +constexpr ULONG kRenderTopologyBridgePin = 0; +constexpr ULONG kRenderTopologyEndpointPin = 1; + +constexpr ULONG kCaptureWaveBridgePin = 0; +constexpr ULONG kCaptureWaveHostPin = 1; +constexpr ULONG kCaptureTopologyEndpointPin = 0; +constexpr ULONG kCaptureTopologyBridgePin = 1; + +constexpr wchar_t kWaveRenderName[] = L"WaveRender"; +constexpr wchar_t kTopologyRenderName[] = L"TopologyRender"; +constexpr wchar_t kWaveCaptureName[] = L"WaveCapture"; +constexpr wchar_t kTopologyCaptureName[] = L"TopologyCapture"; diff --git a/idd/LGAudio/Driver.cpp b/idd/LGAudio/Driver.cpp new file mode 100644 index 00000000..09afb44a --- /dev/null +++ b/idd/LGAudio/Driver.cpp @@ -0,0 +1,99 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "Common.h" +#include "Adapter.h" + +extern "C" DRIVER_INITIALIZE DriverEntry; + +namespace +{ +NTSTATUS AddAudioDevice( + _In_ PDRIVER_OBJECT driverObject, + _In_ PDEVICE_OBJECT physicalDeviceObject) +{ + constexpr ULONG kSubdeviceCount = 4; + return PcAddAdapterDevice(driverObject, physicalDeviceObject, + StartAudioDevice, kSubdeviceCount, 0); +} +} + +extern "C" NTSTATUS DriverEntry( + _In_ PDRIVER_OBJECT driverObject, + _In_ PUNICODE_STRING registryPath) +{ + return PcInitializeAdapterDriver(driverObject, registryPath, + AddAudioDevice); +} + +#pragma warning(push) +#pragma warning(disable: 4996) +void * __cdecl operator new( + size_t size, + POOL_TYPE poolType, + ULONG tag) +{ + void *memory = ExAllocatePoolWithTag(poolType, size, tag); + if (memory) + RtlZeroMemory(memory, size); + return memory; +} + +void * __cdecl operator new( + size_t size, + POOL_FLAGS poolFlags, + ULONG tag) +{ + const POOL_TYPE poolType = (poolFlags & POOL_FLAG_PAGED) ? + PagedPool : NonPagedPoolNx; + return operator new(size, poolType, tag); +} +#pragma warning(pop) + +void __cdecl operator delete(void *memory) noexcept +{ + if (memory) + ExFreePool(memory); +} + +void __cdecl operator delete(void *memory, size_t size) noexcept +{ + UNREFERENCED_PARAMETER(size); + operator delete(memory); +} + +void __cdecl operator delete( + void *memory, + POOL_TYPE poolType, + ULONG tag) noexcept +{ + UNREFERENCED_PARAMETER(poolType); + UNREFERENCED_PARAMETER(tag); + operator delete(memory); +} +void __cdecl operator delete( + void *memory, + POOL_FLAGS poolFlags, + ULONG tag) noexcept +{ + UNREFERENCED_PARAMETER(poolFlags); + UNREFERENCED_PARAMETER(tag); + operator delete(memory); +} diff --git a/idd/LGAudio/Formats.cpp b/idd/LGAudio/Formats.cpp new file mode 100644 index 00000000..6ae4af8c --- /dev/null +++ b/idd/LGAudio/Formats.cpp @@ -0,0 +1,455 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "Formats.h" + +namespace LGAudio +{ + namespace + { + constexpr USHORT kExtensibleExtraSize = static_cast( + sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)); + + bool IsAudioPcmWaveFormat(_In_ const KSDATAFORMAT * format) + { + return + IsEqualGUIDAligned(format->MajorFormat, + KSDATAFORMAT_TYPE_AUDIO) && + IsEqualGUIDAligned(format->SubFormat, + KSDATAFORMAT_SUBTYPE_PCM) && + IsEqualGUIDAligned(format->Specifier, + KSDATAFORMAT_SPECIFIER_WAVEFORMATEX); + } + + bool WaveFormatCoreMatches( + _In_ const WAVEFORMATEX & candidate, + _In_ const WAVEFORMATEX & requested) + { + return + candidate.nChannels == requested.nChannels && + candidate.nSamplesPerSec == requested.nSamplesPerSec && + candidate.nAvgBytesPerSec == requested.nAvgBytesPerSec && + candidate.nBlockAlign == requested.nBlockAlign && + candidate.wBitsPerSample == requested.wBitsPerSample; + } + + bool ExtensibleFormatMatches( + _In_ const WAVEFORMATEXTENSIBLE & candidate, + _In_ const WAVEFORMATEXTENSIBLE & requested) + { + return + WaveFormatCoreMatches(candidate.Format, requested.Format) && + candidate.Samples.wValidBitsPerSample == + requested.Samples.wValidBitsPerSample && + candidate.dwChannelMask == requested.dwChannelMask && + IsEqualGUIDAligned(candidate.SubFormat, requested.SubFormat); + } + + void GetFormatList( + _In_ EndpointKind endpoint, + _Outptr_result_buffer_(*count) + KSDATAFORMAT_WAVEFORMATEXTENSIBLE ** formats, + _Out_ PULONG count) + { + if (endpoint == EndpointKind::Render) + { + *formats = g_renderFormats; + *count = kRenderFormatCount; + } + else + { + *formats = g_captureFormats; + *count = kCaptureFormatCount; + } + } + + void GetDataRangeList( + _In_ EndpointKind endpoint, + _Outptr_result_buffer_(*count) KSDATARANGE_AUDIO ** ranges, + _Out_ PULONG count) + { + if (endpoint == EndpointKind::Render) + { + *ranges = g_renderDataRanges; + *count = kRenderFormatCount; + } + else + { + *ranges = g_captureDataRanges; + *count = kCaptureFormatCount; + } + } + + bool AudioRangeContainsFormat( + _In_ const KSDATARANGE_AUDIO & range, + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE & format) + { + const WAVEFORMATEX & wave = format.WaveFormatExt.Format; + + return + range.MaximumChannels == wave.nChannels && + range.MinimumBitsPerSample <= wave.wBitsPerSample && + range.MaximumBitsPerSample >= wave.wBitsPerSample && + range.MinimumSampleFrequency <= wave.nSamplesPerSec && + range.MaximumSampleFrequency >= wave.nSamplesPerSec; + } + + bool DataRangeIdentifiesFormat( + _In_ const KSDATARANGE_AUDIO & range, + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE & format) + { + const WAVEFORMATEX & wave = format.WaveFormatExt.Format; + + return + range.MaximumChannels == wave.nChannels && + range.MinimumBitsPerSample == wave.wBitsPerSample && + range.MaximumBitsPerSample == wave.wBitsPerSample && + range.MinimumSampleFrequency == wave.nSamplesPerSec && + range.MaximumSampleFrequency == wave.nSamplesPerSec; + } + } + +#define LG_AUDIO_FORMAT( \ + sampleRate_, channels_, containerBits_, validBits_, mask_) \ + { \ + { \ + sizeof(KSDATAFORMAT_WAVEFORMATEXTENSIBLE), \ + 0, \ + 0, \ + 0, \ + STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO), \ + STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM), \ + STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX) \ + }, \ + { \ + { \ + WAVE_FORMAT_EXTENSIBLE, \ + channels_, \ + sampleRate_, \ + sampleRate_ * channels_ * (containerBits_ / 8), \ + channels_ * (containerBits_ / 8), \ + containerBits_, \ + kExtensibleExtraSize \ + }, \ + validBits_, \ + mask_, \ + STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM) \ + } \ + } + +#define LG_RENDER_FORMATS(sampleRate_) \ + LG_AUDIO_FORMAT(sampleRate_, 2, 16, 16, KSAUDIO_SPEAKER_STEREO), \ + LG_AUDIO_FORMAT(sampleRate_, 2, 24, 24, KSAUDIO_SPEAKER_STEREO), \ + LG_AUDIO_FORMAT(sampleRate_, 6, 24, 24, \ + KSAUDIO_SPEAKER_5POINT1_SURROUND), \ + LG_AUDIO_FORMAT(sampleRate_, 8, 24, 24, \ + KSAUDIO_SPEAKER_7POINT1_SURROUND) + +#define LG_CAPTURE_FORMATS(sampleRate_) \ + LG_AUDIO_FORMAT(sampleRate_, 1, 16, 16, KSAUDIO_SPEAKER_MONO), \ + LG_RENDER_FORMATS(sampleRate_) + + KSDATAFORMAT_WAVEFORMATEXTENSIBLE + g_renderFormats[kRenderFormatCount] = + { + LG_RENDER_FORMATS(48000), + LG_RENDER_FORMATS(44100), + LG_RENDER_FORMATS(88200), + LG_RENDER_FORMATS(96000), + LG_RENDER_FORMATS(176400), + LG_RENDER_FORMATS(192000) + }; + + KSDATAFORMAT_WAVEFORMATEXTENSIBLE + g_captureFormats[kCaptureFormatCount] = + { + LG_CAPTURE_FORMATS(48000), + LG_CAPTURE_FORMATS(44100), + LG_CAPTURE_FORMATS(88200), + LG_CAPTURE_FORMATS(96000), + LG_CAPTURE_FORMATS(176400), + LG_CAPTURE_FORMATS(192000) + }; + +#undef LG_CAPTURE_FORMATS +#undef LG_RENDER_FORMATS +#undef LG_AUDIO_FORMAT + +#define LG_AUDIO_RANGE(sampleRate_, channels_, containerBits_) \ + { \ + { \ + sizeof(KSDATARANGE_AUDIO), \ + 0, \ + 0, \ + 0, \ + STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO), \ + STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM), \ + STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX) \ + }, \ + channels_, \ + containerBits_, \ + containerBits_, \ + sampleRate_, \ + sampleRate_ \ + } + +#define LG_RENDER_RANGES(sampleRate_) \ + LG_AUDIO_RANGE(sampleRate_, 2, 16), \ + LG_AUDIO_RANGE(sampleRate_, 2, 24), \ + LG_AUDIO_RANGE(sampleRate_, 6, 24), \ + LG_AUDIO_RANGE(sampleRate_, 8, 24) + +#define LG_CAPTURE_RANGES(sampleRate_) \ + LG_AUDIO_RANGE(sampleRate_, 1, 16), \ + LG_RENDER_RANGES(sampleRate_) + + KSDATARANGE_AUDIO g_renderDataRanges[kRenderFormatCount] = + { + LG_RENDER_RANGES(48000), + LG_RENDER_RANGES(44100), + LG_RENDER_RANGES(88200), + LG_RENDER_RANGES(96000), + LG_RENDER_RANGES(176400), + LG_RENDER_RANGES(192000) + }; + + KSDATARANGE_AUDIO g_captureDataRanges[kCaptureFormatCount] = + { + LG_CAPTURE_RANGES(48000), + LG_CAPTURE_RANGES(44100), + LG_CAPTURE_RANGES(88200), + LG_CAPTURE_RANGES(96000), + LG_CAPTURE_RANGES(176400), + LG_CAPTURE_RANGES(192000) + }; + +#undef LG_CAPTURE_RANGES +#undef LG_RENDER_RANGES +#undef LG_AUDIO_RANGE + +#define LG_RENDER_RANGE_POINTERS(base_) \ + (PKSDATARANGE)&g_renderDataRanges[(base_)], \ + (PKSDATARANGE)&g_renderDataRanges[(base_) + 1], \ + (PKSDATARANGE)&g_renderDataRanges[(base_) + 2], \ + (PKSDATARANGE)&g_renderDataRanges[(base_) + 3] + +#define LG_CAPTURE_RANGE_POINTERS(base_) \ + (PKSDATARANGE)&g_captureDataRanges[(base_)], \ + (PKSDATARANGE)&g_captureDataRanges[(base_) + 1], \ + (PKSDATARANGE)&g_captureDataRanges[(base_) + 2], \ + (PKSDATARANGE)&g_captureDataRanges[(base_) + 3], \ + (PKSDATARANGE)&g_captureDataRanges[(base_) + 4] + + PKSDATARANGE g_renderDataRangePointers[kRenderFormatCount] = + { + LG_RENDER_RANGE_POINTERS(0), + LG_RENDER_RANGE_POINTERS(4), + LG_RENDER_RANGE_POINTERS(8), + LG_RENDER_RANGE_POINTERS(12), + LG_RENDER_RANGE_POINTERS(16), + LG_RENDER_RANGE_POINTERS(20) + }; + + PKSDATARANGE g_captureDataRangePointers[kCaptureFormatCount] = + { + LG_CAPTURE_RANGE_POINTERS(0), + LG_CAPTURE_RANGE_POINTERS(5), + LG_CAPTURE_RANGE_POINTERS(10), + LG_CAPTURE_RANGE_POINTERS(15), + LG_CAPTURE_RANGE_POINTERS(20), + LG_CAPTURE_RANGE_POINTERS(25) + }; + +#undef LG_CAPTURE_RANGE_POINTERS +#undef LG_RENDER_RANGE_POINTERS + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS ValidateFormat( + _In_ EndpointKind endpoint, + _In_ const KSDATAFORMAT * dataFormat, + _Outptr_opt_result_maybenull_ + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE ** matchedFormat) + { + if (matchedFormat) + *matchedFormat = nullptr; + + if (!dataFormat || + dataFormat->FormatSize < sizeof(KSDATAFORMAT_WAVEFORMATEX) || + !IsAudioPcmWaveFormat(dataFormat)) + return STATUS_NO_MATCH; + + const WAVEFORMATEX * requested = + reinterpret_cast(dataFormat + 1); + + KSDATAFORMAT_WAVEFORMATEXTENSIBLE * formats; + ULONG count; + GetFormatList(endpoint, &formats, &count); + + for (ULONG i = 0; i < count; ++i) + { + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE & candidate = formats[i]; + const WAVEFORMATEXTENSIBLE & candidateWave = candidate.WaveFormatExt; + + if (!WaveFormatCoreMatches(candidateWave.Format, *requested)) + continue; + + if (requested->wFormatTag == WAVE_FORMAT_PCM) + { + // A stand-alone WAVEFORMATEX is unambiguous only for these legacy + // mono/stereo, 16-bit formats. + if (candidateWave.Format.wBitsPerSample != 16 || + candidateWave.Samples.wValidBitsPerSample != 16 || + candidateWave.Format.nChannels > 2) + continue; + } + else if (requested->wFormatTag == WAVE_FORMAT_EXTENSIBLE) + { + if (dataFormat->FormatSize < + sizeof(KSDATAFORMAT_WAVEFORMATEXTENSIBLE) || + requested->cbSize < kExtensibleExtraSize) + continue; + + const WAVEFORMATEXTENSIBLE * requestedExtensible = + reinterpret_cast(requested); + if (!ExtensibleFormatMatches(candidateWave, *requestedExtensible)) + continue; + } + else + continue; + + if (matchedFormat) + *matchedFormat = &candidate; + return STATUS_SUCCESS; + } + + return STATUS_NO_MATCH; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS SelectFormatIntersection( + _In_ EndpointKind endpoint, + _In_ const KSDATARANGE * clientDataRange, + _In_ const KSDATARANGE * driverDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + PVOID resultantFormat, + _Out_ PULONG resultantFormatLength) + { + if (!clientDataRange || !driverDataRange || !resultantFormatLength) + return STATUS_INVALID_PARAMETER; + + *resultantFormatLength = 0; + + if (clientDataRange->FormatSize < sizeof(KSDATARANGE_AUDIO) || + driverDataRange->FormatSize < sizeof(KSDATARANGE_AUDIO) || + !IsAudioPcmWaveFormat(clientDataRange) || + !IsAudioPcmWaveFormat(driverDataRange)) + return STATUS_NO_MATCH; + + const KSDATARANGE_AUDIO & client = + *reinterpret_cast(clientDataRange); + const KSDATARANGE_AUDIO & driver = + *reinterpret_cast(driverDataRange); + + KSDATAFORMAT_WAVEFORMATEXTENSIBLE * formats; + KSDATARANGE_AUDIO * ranges; + ULONG formatCount; + ULONG rangeCount; + GetFormatList(endpoint, &formats, &formatCount); + GetDataRangeList(endpoint, &ranges, &rangeCount); + + if (formatCount != rangeCount) + return STATUS_INTERNAL_ERROR; + + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE * selected = nullptr; + for (ULONG i = 0; i < formatCount; ++i) + { + if (!DataRangeIdentifiesFormat(driver, formats[i]) || + !DataRangeIdentifiesFormat(ranges[i], formats[i]) || + !AudioRangeContainsFormat(client, formats[i])) + continue; + + selected = &formats[i]; + break; + } + + if (!selected) + return STATUS_NO_MATCH; + + constexpr ULONG requiredSize = + sizeof(KSDATAFORMAT_WAVEFORMATEXTENSIBLE); + *resultantFormatLength = requiredSize; + + if (outputBufferLength == 0) + return STATUS_BUFFER_OVERFLOW; + if (outputBufferLength < requiredSize) + return STATUS_BUFFER_TOO_SMALL; + if (!resultantFormat) + return STATUS_INVALID_PARAMETER; + + RtlCopyMemory(resultantFormat, selected, requiredSize); + return STATUS_SUCCESS; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS GetFormatFingerprint( + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE * dataFormat, + _Out_ FormatFingerprint * fingerprint) + { + if (!dataFormat || !fingerprint) + return STATUS_INVALID_PARAMETER; + + const WAVEFORMATEXTENSIBLE & wave = dataFormat->WaveFormatExt; + if (wave.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE || + wave.Format.cbSize < kExtensibleExtraSize || + wave.Format.nChannels == 0 || + wave.Format.nBlockAlign == 0 || + wave.Format.wBitsPerSample == 0 || + wave.Samples.wValidBitsPerSample == 0 || + wave.Samples.wValidBitsPerSample > + wave.Format.wBitsPerSample || + !IsAudioPcmWaveFormat(&dataFormat->DataFormat) || + !IsEqualGUIDAligned(wave.SubFormat, + KSDATAFORMAT_SUBTYPE_PCM)) + return STATUS_INVALID_PARAMETER; + + fingerprint->sampleRate = wave.Format.nSamplesPerSec; + fingerprint->channelMask = wave.dwChannelMask; + fingerprint->channels = wave.Format.nChannels; + fingerprint->containerBits = wave.Format.wBitsPerSample; + fingerprint->validBits = wave.Samples.wValidBitsPerSample; + fingerprint->blockAlign = wave.Format.nBlockAlign; + return STATUS_SUCCESS; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + bool EqualFormatFingerprint( + _In_ const FormatFingerprint & left, + _In_ const FormatFingerprint & right) + { + return + left.sampleRate == right.sampleRate && + left.channelMask == right.channelMask && + left.channels == right.channels && + left.containerBits == right.containerBits && + left.validBits == right.validBits && + left.blockAlign == right.blockAlign; + } +} diff --git a/idd/LGAudio/Formats.h b/idd/LGAudio/Formats.h new file mode 100644 index 00000000..ca734fc3 --- /dev/null +++ b/idd/LGAudio/Formats.h @@ -0,0 +1,125 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include +#include +#include + +namespace LGAudio +{ + enum class EndpointKind : UCHAR + { + Render, + Capture + }; + + struct FormatFingerprint + { + ULONG sampleRate; + ULONG channelMask; + USHORT channels; + USHORT containerBits; + USHORT validBits; + USHORT blockAlign; + }; + + constexpr ULONG kSampleRate = 48000; + constexpr ULONG kSampleRateCount = 6; + constexpr ULONG kRenderFormatsPerRate = 4; + constexpr ULONG kCaptureFormatsPerRate = 5; + constexpr ULONG kRenderFormatCount = + kSampleRateCount * kRenderFormatsPerRate; + constexpr ULONG kCaptureFormatCount = + kSampleRateCount * kCaptureFormatsPerRate; + constexpr ULONG kMaximumSampleRate = 192000; + constexpr ULONG kMaximumBlockAlign = + 8 * 3; + constexpr ULONG kMaximumBytesPerSecond = + kMaximumSampleRate * kMaximumBlockAlign; + + extern KSDATAFORMAT_WAVEFORMATEXTENSIBLE + g_renderFormats[kRenderFormatCount]; + extern KSDATAFORMAT_WAVEFORMATEXTENSIBLE + g_captureFormats[kCaptureFormatCount]; + + extern KSDATARANGE_AUDIO g_renderDataRanges[kRenderFormatCount]; + extern KSDATARANGE_AUDIO g_captureDataRanges[kCaptureFormatCount]; + + extern PKSDATARANGE g_renderDataRangePointers[kRenderFormatCount]; + extern PKSDATARANGE g_captureDataRangePointers[kCaptureFormatCount]; + + /** + * Validate an instantiated WaveRT format against the endpoint's exact + * discrete format list. + */ + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS ValidateFormat( + _In_ EndpointKind endpoint, + _In_ const KSDATAFORMAT * dataFormat, + _Outptr_opt_result_maybenull_ + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE ** matchedFormat); + + /** + * Select the exact WAVEFORMATEXTENSIBLE result for an IMiniport data-range + * intersection request. + */ + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS SelectFormatIntersection( + _In_ EndpointKind endpoint, + _In_ const KSDATARANGE * clientDataRange, + _In_ const KSDATARANGE * driverDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + PVOID resultantFormat, + _Out_ PULONG resultantFormatLength); + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS GetFormatFingerprint( + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE * dataFormat, + _Out_ FormatFingerprint * fingerprint); + + _IRQL_requires_max_(DISPATCH_LEVEL) + bool EqualFormatFingerprint( + _In_ const FormatFingerprint & left, + _In_ const FormatFingerprint & right); +} + +// The rest of the driver uses these as its shared, unqualified audio types. +using LGAudio::EndpointKind; +using LGAudio::FormatFingerprint; +using LGAudio::EqualFormatFingerprint; +using LGAudio::GetFormatFingerprint; +using LGAudio::SelectFormatIntersection; +using LGAudio::ValidateFormat; +using LGAudio::g_captureDataRangePointers; +using LGAudio::g_captureDataRanges; +using LGAudio::g_captureFormats; +using LGAudio::g_renderDataRangePointers; +using LGAudio::g_renderDataRanges; +using LGAudio::g_renderFormats; +using LGAudio::kCaptureFormatCount; +using LGAudio::kMaximumBlockAlign; +using LGAudio::kMaximumBytesPerSecond; +using LGAudio::kMaximumSampleRate; +using LGAudio::kRenderFormatCount; +using LGAudio::kSampleRate; +using LGAudio::kSampleRateCount; diff --git a/idd/LGAudio/Guids.cpp b/idd/LGAudio/Guids.cpp new file mode 100644 index 00000000..88b74214 --- /dev/null +++ b/idd/LGAudio/Guids.cpp @@ -0,0 +1,22 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include "Common.h" diff --git a/idd/LGAudio/Guids.h b/idd/LGAudio/Guids.h new file mode 100644 index 00000000..3c09882d --- /dev/null +++ b/idd/LGAudio/Guids.h @@ -0,0 +1,23 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +// Standard PortCls, KS, and audio GUID storage is emitted by Guids.cpp. diff --git a/idd/LGAudio/LGAudio.inx b/idd/LGAudio/LGAudio.inx new file mode 100644 index 00000000..95dde2f4 --- /dev/null +++ b/idd/LGAudio/LGAudio.inx @@ -0,0 +1,125 @@ +[Version] +Signature="$Windows NT$" +Class=MEDIA +ClassGuid={4D36E96C-E325-11CE-BFC1-08002BE10318} +Provider=%ProviderName% +DriverVer=08/21/2026,0.1.0.0 +CatalogFile=LGAudio.cat +PnpLockDown=1 + +[SourceDisksNames] +1=%DiskName%,,, + +[SourceDisksFiles] +LGAudio.sys=1 + +[Manufacturer] +%ManufacturerName%=LGAudio,NT$ARCH$.10.0...16299 + +[LGAudio.NT$ARCH$.10.0...16299] +%DeviceName%=LGAudio_Device,Root\LGAudio + +[DestinationDirs] +LGAudio_CopyFiles=13 + +[LGAudio_CopyFiles] +LGAudio.sys + +[LGAudio_Device.NT] +Include=ks.inf,wdmaudio.inf +Needs=KS.Registration,WDMAUDIO.Registration +CopyFiles=LGAudio_CopyFiles +AddReg=LGAudio_Device.AddReg + +[LGAudio_Device.AddReg] +HKR,,AssociatedFilters,,"wdmaud,swmidi,redbook" +HKR,,Driver,,LGAudio.sys +HKR,Drivers,SubClasses,,"wave,midi,mixer" +HKR,Drivers\wave\wdmaud.drv,Driver,,wdmaud.drv +HKR,Drivers\midi\wdmaud.drv,Driver,,wdmaud.drv +HKR,Drivers\mixer\wdmaud.drv,Driver,,wdmaud.drv +HKR,Drivers\wave\wdmaud.drv,Description,,%DeviceName% +HKR,Drivers\mixer\wdmaud.drv,Description,,%DeviceName% + +[LGAudio_Device.NT.Interfaces] +AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_WaveRender%,LGAudio.WaveRender +AddInterface=%KSCATEGORY_RENDER%,%KSNAME_WaveRender%,LGAudio.WaveRender +AddInterface=%KSCATEGORY_REALTIME%,%KSNAME_WaveRender%,LGAudio.WaveRender +AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_TopologyRender%,LGAudio.TopologyRender +AddInterface=%KSCATEGORY_TOPOLOGY%,%KSNAME_TopologyRender%,LGAudio.TopologyRender +AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_WaveCapture%,LGAudio.WaveCapture +AddInterface=%KSCATEGORY_CAPTURE%,%KSNAME_WaveCapture%,LGAudio.WaveCapture +AddInterface=%KSCATEGORY_REALTIME%,%KSNAME_WaveCapture%,LGAudio.WaveCapture +AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_TopologyCapture%,LGAudio.TopologyCapture +AddInterface=%KSCATEGORY_TOPOLOGY%,%KSNAME_TopologyCapture%,LGAudio.TopologyCapture + +[LGAudio.WaveRender] +AddReg=LGAudio.WaveRender.AddReg + +[LGAudio.WaveRender.AddReg] +HKR,,CLSID,,%Proxy.CLSID% +HKR,,FriendlyName,,%EndpointName% + +[LGAudio.TopologyRender] +AddReg=LGAudio.TopologyRender.AddReg + +[LGAudio.TopologyRender.AddReg] +HKR,,CLSID,,%Proxy.CLSID% +HKR,,FriendlyName,,%EndpointName% +HKR,EP\0,%PKEY_AudioEndpoint_Association%,,%KSNODETYPE_ANY% +HKR,EP\0,%PKEY_AudioEndpoint_Supports_EventDriven_Mode%,0x00010001,1 + +[LGAudio.WaveCapture] +AddReg=LGAudio.WaveCapture.AddReg + +[LGAudio.WaveCapture.AddReg] +HKR,,CLSID,,%Proxy.CLSID% +HKR,,FriendlyName,,%EndpointName% + +[LGAudio.TopologyCapture] +AddReg=LGAudio.TopologyCapture.AddReg + +[LGAudio.TopologyCapture.AddReg] +HKR,,CLSID,,%Proxy.CLSID% +HKR,,FriendlyName,,%EndpointName% +HKR,EP\0,%PKEY_AudioEndpoint_Association%,,%KSNODETYPE_ANY% +HKR,EP\0,%PKEY_AudioEndpoint_Supports_EventDriven_Mode%,0x00010001,1 + +[LGAudio_Device.NT.Services] +AddService=LGAudio,0x00000002,LGAudio_Service + +[LGAudio_Service] +DisplayName=%ServiceName% +ServiceType=1 +StartType=3 +ErrorControl=1 +ServiceBinary=%13%\LGAudio.sys + +[LGAudio_Device.NT.HW] +AddReg=LGAudio_HardwareSettings + +[LGAudio_HardwareSettings] +HKR,,DeviceType,0x00010001,0x0000001D +HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;WD)(A;;GRGWGX;;;RC)" + +[Strings] +ProviderName="Looking Glass" +ManufacturerName="Looking Glass" +DiskName="Looking Glass Audio Installation Disk" +DeviceName="Looking Glass Audio" +EndpointName="Looking Glass Audio" +ServiceName="Looking Glass Audio Driver" + +KSNAME_WaveRender="WaveRender" +KSNAME_TopologyRender="TopologyRender" +KSNAME_WaveCapture="WaveCapture" +KSNAME_TopologyCapture="TopologyCapture" +Proxy.CLSID="{17CCA71B-ECD7-11D0-B908-00A0C9223196}" +KSCATEGORY_AUDIO="{6994AD04-93EF-11D0-A3CC-00A0C9223196}" +KSCATEGORY_RENDER="{65E8773E-8F56-11D0-A3B9-00A0C9223196}" +KSCATEGORY_CAPTURE="{65E8773D-8F56-11D0-A3B9-00A0C9223196}" +KSCATEGORY_REALTIME="{EB115FFC-10C8-4964-831D-6DCB02E6F23F}" +KSCATEGORY_TOPOLOGY="{DDA54A40-1E4C-11D1-A050-405705C10000}" +KSNODETYPE_ANY="{00000000-0000-0000-0000-000000000000}" +PKEY_AudioEndpoint_Association="{1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E},2" +PKEY_AudioEndpoint_Supports_EventDriven_Mode="{1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E},7" diff --git a/idd/LGAudio/LGAudio.rc b/idd/LGAudio/LGAudio.rc new file mode 100644 index 00000000..41dfe29e --- /dev/null +++ b/idd/LGAudio/LGAudio.rc @@ -0,0 +1,31 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include + +#define VER_FILETYPE VFT_DRV +#define VER_FILESUBTYPE VFT2_DRV_SOUND +#define VER_FILEDESCRIPTION_STR "Looking Glass Audio Driver" +#define VER_INTERNALNAME_STR "LGAudio.sys" +#define VER_ORIGINALFILENAME_STR "LGAudio.sys" +#define VER_LEGALCOPYRIGHT_STR "Copyright (C) 2026 Looking Glass" + +#include "common.ver" diff --git a/idd/LGAudio/LGAudio.vcxproj b/idd/LGAudio/LGAudio.vcxproj new file mode 100644 index 00000000..9e6cfb16 --- /dev/null +++ b/idd/LGAudio/LGAudio.vcxproj @@ -0,0 +1,104 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {02146E7A-FA7C-4071-8759-8B9A89452367} + LGAudio + 10.0.26100.0 + Debug + x64 + + + + Windows10 + Universal + + <_NT_TARGET_VERSION>0xA000004 + WDM + WindowsKernelModeDriver10.0 + Driver + Spectre + + + true + + + false + true + + + + + + + + + LGAudio + DbgengKernelDebugger + true + /sw2084 + + + + $(ProjectDir);$(DDK_INC_PATH);%(AdditionalIncludeDirectories) + _WIN32;_WIN32_WINNT=0x0A00;UNICODE;_UNICODE;PC_IMPLEMENTATION;_USE_WAVERT_;_NEW_DELETE_OPERATORS_;%(PreprocessorDefinitions) + stdcpp17 + Level4 + true + false + + + $(DDK_INC_PATH);%(AdditionalIncludeDirectories) + _WIN32;_WIN32_WINNT=0x0A00;UNICODE;_UNICODE;PC_IMPLEMENTATION;_USE_WAVERT_;%(PreprocessorDefinitions) + + + portcls.lib;stdunk.lib;libcntpr.lib;%(AdditionalDependencies) + + + SHA256 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idd/LGAudio/LGAudio.vcxproj.filters b/idd/LGAudio/LGAudio.vcxproj.filters new file mode 100644 index 00000000..04b66721 --- /dev/null +++ b/idd/LGAudio/LGAudio.vcxproj.filters @@ -0,0 +1,46 @@ + + + + + {64B60D55-6E76-4D04-9AE1-F23B2ECA8541} + + + {770F1246-D1F5-46C0-B3F6-C645E10435C0} + + + {A1D5B8F7-A246-4E24-8CCB-743901B04FE4} + + + {5E4626AA-3871-4CDD-B0F0-71315BC11643} + + + + Driver + Driver + Driver + Driver + Miniport + Miniport + Miniport + Utilities + Utilities + + + Driver + Driver + Driver + Driver + Miniport + Miniport + Miniport + Utilities + Utilities + + + Package + + Package + + + diff --git a/idd/LGAudio/MiniportTopology.cpp b/idd/LGAudio/MiniportTopology.cpp new file mode 100644 index 00000000..b4c0f3bd --- /dev/null +++ b/idd/LGAudio/MiniportTopology.cpp @@ -0,0 +1,231 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "MiniportTopology.h" + +namespace +{ +KSDATARANGE g_AnalogRange = +{ + sizeof(KSDATARANGE), + 0, + 0, + 0, + STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO), + STATICGUIDOF(KSDATAFORMAT_SUBTYPE_ANALOG), + STATICGUIDOF(KSDATAFORMAT_SPECIFIER_NONE) +}; + +PKSDATARANGE g_AnalogRanges[] = +{ + &g_AnalogRange +}; + +PCPIN_DESCRIPTOR g_RenderPins[] = +{ + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_AnalogRanges), g_AnalogRanges, + KSPIN_DATAFLOW_IN, + KSPIN_COMMUNICATION_NONE, + &KSCATEGORY_AUDIO, + nullptr, + 0 + } + }, + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_AnalogRanges), g_AnalogRanges, + KSPIN_DATAFLOW_OUT, + KSPIN_COMMUNICATION_NONE, + &KSNODETYPE_SPEAKER, + nullptr, + 0 + } + } +}; + +PCCONNECTION_DESCRIPTOR g_RenderConnections[] = +{ + { + PCFILTER_NODE, + kRenderTopologyBridgePin, + PCFILTER_NODE, + kRenderTopologyEndpointPin + } +}; + +PCFILTER_DESCRIPTOR g_RenderDescriptor = +{ + 0, + nullptr, + sizeof(PCPIN_DESCRIPTOR), + ARRAYSIZE(g_RenderPins), + g_RenderPins, + sizeof(PCNODE_DESCRIPTOR), + 0, + nullptr, + ARRAYSIZE(g_RenderConnections), + g_RenderConnections, + 0, + nullptr +}; + +PCPIN_DESCRIPTOR g_CapturePins[] = +{ + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_AnalogRanges), g_AnalogRanges, + KSPIN_DATAFLOW_IN, + KSPIN_COMMUNICATION_NONE, + &KSNODETYPE_MICROPHONE, + nullptr, + 0 + } + }, + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_AnalogRanges), g_AnalogRanges, + KSPIN_DATAFLOW_OUT, + KSPIN_COMMUNICATION_NONE, + &KSCATEGORY_AUDIO, + nullptr, + 0 + } + } +}; + +PCCONNECTION_DESCRIPTOR g_CaptureConnections[] = +{ + { + PCFILTER_NODE, + kCaptureTopologyEndpointPin, + PCFILTER_NODE, + kCaptureTopologyBridgePin + } +}; + +PCFILTER_DESCRIPTOR g_CaptureDescriptor = +{ + 0, + nullptr, + sizeof(PCPIN_DESCRIPTOR), + ARRAYSIZE(g_CapturePins), + g_CapturePins, + sizeof(PCNODE_DESCRIPTOR), + 0, + nullptr, + ARRAYSIZE(g_CaptureConnections), + g_CaptureConnections, + 0, + nullptr +}; +} + +CMiniportTopology::CMiniportTopology(EndpointKind kind) : + m_refCount(1), + m_kind(kind) +{ +} + +NTSTATUS CMiniportTopology::QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) +{ + if (!object) + return STATUS_INVALID_PARAMETER; + + *object = nullptr; + if (GuidEqual(interfaceId, IID_IUnknown) || + GuidEqual(interfaceId, IID_IMiniport) || + GuidEqual(interfaceId, IID_IMiniportTopology)) + { + *object = static_cast(this); + AddRef(); + return STATUS_SUCCESS; + } + + return STATUS_NOINTERFACE; +} + +ULONG CMiniportTopology::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refCount)); +} + +ULONG CMiniportTopology::Release() +{ + const LONG count = InterlockedDecrement(&m_refCount); + if (!count) + delete this; + return static_cast(count); +} + +NTSTATUS CMiniportTopology::GetDescription( + _Out_ PPCFILTER_DESCRIPTOR *description) +{ + if (!description) + return STATUS_INVALID_PARAMETER; + + *description = m_kind == EndpointKind::Render ? + &g_RenderDescriptor : &g_CaptureDescriptor; + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportTopology::DataRangeIntersection( + _In_ ULONG pinId, + _In_ PKSDATARANGE dataRange, + _In_ PKSDATARANGE matchingDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + void *resultantFormat, + _Out_ ULONG *resultantFormatLength) +{ + UNREFERENCED_PARAMETER(pinId); + UNREFERENCED_PARAMETER(dataRange); + UNREFERENCED_PARAMETER(matchingDataRange); + UNREFERENCED_PARAMETER(outputBufferLength); + UNREFERENCED_PARAMETER(resultantFormat); + UNREFERENCED_PARAMETER(resultantFormatLength); + return STATUS_NOT_IMPLEMENTED; +} + +NTSTATUS CMiniportTopology::Init( + _In_ PUNKNOWN unknownAdapter, + _In_ PRESOURCELIST resourceList, + _In_ PPORTTOPOLOGY port) +{ + UNREFERENCED_PARAMETER(unknownAdapter); + UNREFERENCED_PARAMETER(resourceList); + UNREFERENCED_PARAMETER(port); + return STATUS_SUCCESS; +} diff --git a/idd/LGAudio/MiniportTopology.h b/idd/LGAudio/MiniportTopology.h new file mode 100644 index 00000000..a2c3fad5 --- /dev/null +++ b/idd/LGAudio/MiniportTopology.h @@ -0,0 +1,56 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "Common.h" +#include "Formats.h" + +class CMiniportTopology final : public IMiniportTopology +{ +public: + explicit CMiniportTopology(EndpointKind kind); + ~CMiniportTopology() = default; + + STDMETHODIMP_(NTSTATUS) QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + + STDMETHODIMP_(NTSTATUS) GetDescription( + _Out_ PPCFILTER_DESCRIPTOR *description) override; + STDMETHODIMP_(NTSTATUS) DataRangeIntersection( + _In_ ULONG pinId, + _In_ PKSDATARANGE dataRange, + _In_ PKSDATARANGE matchingDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + void *resultantFormat, + _Out_ ULONG *resultantFormatLength) override; + STDMETHODIMP_(NTSTATUS) Init( + _In_ PUNKNOWN unknownAdapter, + _In_ PRESOURCELIST resourceList, + _In_ PPORTTOPOLOGY port) override; + +private: + volatile LONG m_refCount; + EndpointKind m_kind; +}; diff --git a/idd/LGAudio/MiniportWaveRT.cpp b/idd/LGAudio/MiniportWaveRT.cpp new file mode 100644 index 00000000..96fd8a3a --- /dev/null +++ b/idd/LGAudio/MiniportWaveRT.cpp @@ -0,0 +1,326 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "MiniportWaveRT.h" +#include "MiniportWaveRTStream.h" + +namespace +{ +KSDATARANGE g_BridgeRange = +{ + sizeof(KSDATARANGE), + 0, + 0, + 0, + STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO), + STATICGUIDOF(KSDATAFORMAT_SUBTYPE_ANALOG), + STATICGUIDOF(KSDATAFORMAT_SPECIFIER_NONE) +}; + +PKSDATARANGE g_BridgeRanges[] = +{ + &g_BridgeRange +}; + +PCPIN_DESCRIPTOR g_RenderWavePins[] = +{ + { + 1, 1, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + kRenderFormatCount, g_renderDataRangePointers, + KSPIN_DATAFLOW_IN, + KSPIN_COMMUNICATION_SINK, + &KSCATEGORY_AUDIO, + nullptr, + 0 + } + }, + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_BridgeRanges), g_BridgeRanges, + KSPIN_DATAFLOW_OUT, + KSPIN_COMMUNICATION_NONE, + &KSCATEGORY_AUDIO, + nullptr, + 0 + } + } +}; + +PCCONNECTION_DESCRIPTOR g_RenderWaveConnections[] = +{ + { + PCFILTER_NODE, + kRenderWaveHostPin, + PCFILTER_NODE, + kRenderWaveBridgePin + } +}; + +PCFILTER_DESCRIPTOR g_RenderWaveDescriptor = +{ + 0, + nullptr, + sizeof(PCPIN_DESCRIPTOR), + ARRAYSIZE(g_RenderWavePins), + g_RenderWavePins, + sizeof(PCNODE_DESCRIPTOR), + 0, + nullptr, + ARRAYSIZE(g_RenderWaveConnections), + g_RenderWaveConnections, + 0, + nullptr +}; + +PCPIN_DESCRIPTOR g_CaptureWavePins[] = +{ + { + 0, 0, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + ARRAYSIZE(g_BridgeRanges), g_BridgeRanges, + KSPIN_DATAFLOW_IN, + KSPIN_COMMUNICATION_NONE, + &KSCATEGORY_AUDIO, + nullptr, + 0 + } + }, + { + 1, 1, 0, nullptr, + { + 0, nullptr, + 0, nullptr, + kCaptureFormatCount, g_captureDataRangePointers, + KSPIN_DATAFLOW_OUT, + KSPIN_COMMUNICATION_SINK, + &KSCATEGORY_AUDIO, + &KSAUDFNAME_RECORDING_CONTROL, + 0 + } + } +}; + +PCNODE_DESCRIPTOR g_CaptureWaveNodes[] = +{ + { + 0, + nullptr, + &KSNODETYPE_ADC, + nullptr + } +}; + +PCCONNECTION_DESCRIPTOR g_CaptureWaveConnections[] = +{ + { + PCFILTER_NODE, + kCaptureWaveBridgePin, + 0, + 1 + }, + { + 0, + 0, + PCFILTER_NODE, + kCaptureWaveHostPin + } +}; + +PCFILTER_DESCRIPTOR g_CaptureWaveDescriptor = +{ + 0, + nullptr, + sizeof(PCPIN_DESCRIPTOR), + ARRAYSIZE(g_CaptureWavePins), + g_CaptureWavePins, + sizeof(PCNODE_DESCRIPTOR), + ARRAYSIZE(g_CaptureWaveNodes), + g_CaptureWaveNodes, + ARRAYSIZE(g_CaptureWaveConnections), + g_CaptureWaveConnections, + 0, + nullptr +}; +} + +CMiniportWaveRT::CMiniportWaveRT( + _In_ CAudioAdapter *adapter, + _In_ EndpointKind kind) : + m_refCount(1), + m_streamOpen(0), + m_adapter(adapter), + m_kind(kind) +{ + m_adapter->AddRef(); +} + +CMiniportWaveRT::~CMiniportWaveRT() +{ + m_adapter->Release(); +} + +NTSTATUS CMiniportWaveRT::QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) +{ + if (!object) + return STATUS_INVALID_PARAMETER; + + *object = nullptr; + if (GuidEqual(interfaceId, IID_IUnknown) || + GuidEqual(interfaceId, IID_IMiniport) || + GuidEqual(interfaceId, IID_IMiniportWaveRT)) + { + *object = static_cast(this); + AddRef(); + return STATUS_SUCCESS; + } + + return STATUS_NOINTERFACE; +} + +ULONG CMiniportWaveRT::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refCount)); +} + +ULONG CMiniportWaveRT::Release() +{ + const LONG count = InterlockedDecrement(&m_refCount); + if (!count) + delete this; + return static_cast(count); +} + +NTSTATUS CMiniportWaveRT::GetDescription( + _Out_ PPCFILTER_DESCRIPTOR *description) +{ + if (!description) + return STATUS_INVALID_PARAMETER; + + *description = m_kind == EndpointKind::Render ? + &g_RenderWaveDescriptor : &g_CaptureWaveDescriptor; + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRT::DataRangeIntersection( + _In_ ULONG pinId, + _In_ PKSDATARANGE dataRange, + _In_ PKSDATARANGE matchingDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + void *resultantFormat, + _Out_ ULONG *resultantFormatLength) +{ + const ULONG hostPin = m_kind == EndpointKind::Render ? + kRenderWaveHostPin : kCaptureWaveHostPin; + if (pinId != hostPin) + return STATUS_NOT_IMPLEMENTED; + + return SelectFormatIntersection(m_kind, dataRange, matchingDataRange, + outputBufferLength, resultantFormat, resultantFormatLength); +} + +NTSTATUS CMiniportWaveRT::Init( + _In_ PUNKNOWN unknownAdapter, + _In_ PRESOURCELIST resourceList, + _In_ PPORTWAVERT port) +{ + UNREFERENCED_PARAMETER(unknownAdapter); + UNREFERENCED_PARAMETER(resourceList); + UNREFERENCED_PARAMETER(port); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRT::NewStream( + _Out_ PMINIPORTWAVERTSTREAM *stream, + _In_ PPORTWAVERTSTREAM portStream, + _In_ ULONG pin, + _In_ BOOLEAN capture, + _In_ PKSDATAFORMAT dataFormat) +{ + if (!stream || !portStream || !dataFormat) + return STATUS_INVALID_PARAMETER; + *stream = nullptr; + + const bool isCapture = m_kind == EndpointKind::Capture; + const ULONG hostPin = isCapture ? + kCaptureWaveHostPin : kRenderWaveHostPin; + if (pin != hostPin || (!!capture != isCapture)) + return STATUS_INVALID_PARAMETER; + + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE *matchedFormat = nullptr; + NTSTATUS status = ValidateFormat(m_kind, dataFormat, &matchedFormat); + if (!NT_SUCCESS(status)) + return status; + if (!matchedFormat) + return STATUS_NO_MATCH; + + if (InterlockedCompareExchange(&m_streamOpen, 1, 0) != 0) + return STATUS_DEVICE_BUSY; + + CMiniportWaveRTStream *newStream = + new(NonPagedPoolNx, LGAUDIO_POOL_TAG) CMiniportWaveRTStream( + this, m_adapter, portStream, m_kind); + if (!newStream) + { + InterlockedExchange(&m_streamOpen, 0); + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = newStream->Initialize(matchedFormat); + if (!NT_SUCCESS(status)) + { + newStream->Release(); + return status; + } + + *stream = static_cast(newStream); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRT::GetDeviceDescription( + _Out_ PDEVICE_DESCRIPTION deviceDescription) +{ + if (!deviceDescription) + return STATUS_INVALID_PARAMETER; + + RtlZeroMemory(deviceDescription, sizeof(*deviceDescription)); + deviceDescription->Version = DEVICE_DESCRIPTION_VERSION; + deviceDescription->Master = TRUE; + deviceDescription->ScatterGather = TRUE; + deviceDescription->Dma32BitAddresses = TRUE; + deviceDescription->InterfaceType = PCIBus; + deviceDescription->MaximumLength = MAXULONG; + return STATUS_SUCCESS; +} + +void CMiniportWaveRT::StreamClosed() +{ + InterlockedExchange(&m_streamOpen, 0); +} diff --git a/idd/LGAudio/MiniportWaveRT.h b/idd/LGAudio/MiniportWaveRT.h new file mode 100644 index 00000000..42fed874 --- /dev/null +++ b/idd/LGAudio/MiniportWaveRT.h @@ -0,0 +1,71 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "Adapter.h" +#include "Common.h" +#include "Formats.h" + +class CMiniportWaveRT final : public IMiniportWaveRT +{ +public: + CMiniportWaveRT( + _In_ CAudioAdapter *adapter, + _In_ EndpointKind kind); + ~CMiniportWaveRT(); + + STDMETHODIMP_(NTSTATUS) QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + + STDMETHODIMP_(NTSTATUS) GetDescription( + _Out_ PPCFILTER_DESCRIPTOR *description) override; + STDMETHODIMP_(NTSTATUS) DataRangeIntersection( + _In_ ULONG pinId, + _In_ PKSDATARANGE dataRange, + _In_ PKSDATARANGE matchingDataRange, + _In_ ULONG outputBufferLength, + _Out_writes_bytes_to_opt_(outputBufferLength, *resultantFormatLength) + void *resultantFormat, + _Out_ ULONG *resultantFormatLength) override; + STDMETHODIMP_(NTSTATUS) Init( + _In_ PUNKNOWN unknownAdapter, + _In_ PRESOURCELIST resourceList, + _In_ PPORTWAVERT port) override; + STDMETHODIMP_(NTSTATUS) NewStream( + _Out_ PMINIPORTWAVERTSTREAM *stream, + _In_ PPORTWAVERTSTREAM portStream, + _In_ ULONG pin, + _In_ BOOLEAN capture, + _In_ PKSDATAFORMAT dataFormat) override; + STDMETHODIMP_(NTSTATUS) GetDeviceDescription( + _Out_ PDEVICE_DESCRIPTION deviceDescription) override; + + void StreamClosed(); + +private: + volatile LONG m_refCount; + volatile LONG m_streamOpen; + CAudioAdapter *m_adapter; + EndpointKind m_kind; +}; diff --git a/idd/LGAudio/MiniportWaveRTStream.cpp b/idd/LGAudio/MiniportWaveRTStream.cpp new file mode 100644 index 00000000..c4ed955e --- /dev/null +++ b/idd/LGAudio/MiniportWaveRTStream.cpp @@ -0,0 +1,1026 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "MiniportWaveRTStream.h" +#include "MiniportWaveRT.h" + +namespace +{ +ULONGLONG MinUlonglong(ULONGLONG left, ULONGLONG right) +{ + return left < right ? left : right; +} +} + +CMiniportWaveRTStream::CMiniportWaveRTStream( + _In_ CMiniportWaveRT *miniport, + _In_ CAudioAdapter *adapter, + _In_ PPORTWAVERTSTREAM portStream, + _In_ EndpointKind kind) : + m_refCount(1), + m_miniport(miniport), + m_adapter(adapter), + m_portStream(portStream), + m_kind(kind), + m_format{}, + m_fingerprint{}, + m_state(KSSTATE_STOP), + m_timer(nullptr), + m_hasRun(false), + m_bufferMdl(nullptr), + m_buffer(nullptr), + m_bufferAllocationPending(false), + m_bufferSize(0), + m_notificationCount(0), + m_packetSize(0), + m_notificationEvents{}, + m_notificationEventCount(0), + m_clockFrequency(0), + m_qpcFrequency(0), + m_lastClockPosition(0), + m_lastQpcPosition(0), + m_lastPresentationQpc(0), + m_frameRemainder(0), + m_pendingBytes(0), + m_linearPosition(0), + m_cyclicPosition(0), + m_completedPackets(0), + m_lastReadPacket(~0ULL), + m_lastWritePacket(MAXULONG), + m_eosReceived(false), + m_eosNotificationSent(false), + m_eosLinearPosition(0) +{ + m_miniport->AddRef(); + m_adapter->AddRef(); + + KeInitializeSpinLock(&m_positionLock); +} + +CMiniportWaveRTStream::~CMiniportWaveRTStream() +{ + SetState(KSSTATE_STOP); + + if (m_timer) + { + NT_ASSERT(KeGetCurrentIrql() <= APC_LEVEL); + ExDeleteTimer(m_timer, TRUE, TRUE, nullptr); + m_timer = nullptr; + } + + FreeBuffer(m_bufferMdl); + + m_miniport->StreamClosed(); + m_adapter->Release(); + m_miniport->Release(); +} + +NTSTATUS CMiniportWaveRTStream::Initialize( + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE *format) +{ + if (!format) + return STATUS_INVALID_PARAMETER; + + if (!m_timer) + { + m_timer = ExAllocateTimer(TimerCallback, this, + EX_TIMER_HIGH_RESOLUTION); + if (!m_timer) + return STATUS_INSUFFICIENT_RESOURCES; + } + + FormatFingerprint fingerprint{}; + NTSTATUS status = GetFormatFingerprint(format, &fingerprint); + if (!NT_SUCCESS(status)) + return status; + + m_format = *format; + m_fingerprint = fingerprint; + + if (m_kind == EndpointKind::Render) + status = m_adapter->ConfigureRender(m_fingerprint); + return status; +} + +NTSTATUS CMiniportWaveRTStream::QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) +{ + if (!object) + return STATUS_INVALID_PARAMETER; + + *object = nullptr; + if (GuidEqual(interfaceId, IID_IUnknown) || + GuidEqual(interfaceId, IID_IMiniportWaveRTStream) || + GuidEqual(interfaceId, IID_IMiniportWaveRTStreamNotification)) + { + *object = static_cast(this); + } + else if (m_kind == EndpointKind::Capture && + GuidEqual(interfaceId, IID_IMiniportWaveRTInputStream)) + { + *object = static_cast(this); + } + else if (m_kind == EndpointKind::Render && + GuidEqual(interfaceId, IID_IMiniportWaveRTOutputStream)) + { + *object = static_cast(this); + } + else + { + return STATUS_NOINTERFACE; + } + + AddRef(); + return STATUS_SUCCESS; +} + +ULONG CMiniportWaveRTStream::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refCount)); +} + +ULONG CMiniportWaveRTStream::Release() +{ + const LONG count = InterlockedDecrement(&m_refCount); + if (!count) + delete this; + return static_cast(count); +} + +NTSTATUS CMiniportWaveRTStream::SetFormat( + _In_ PKSDATAFORMAT dataFormat) +{ + if (!dataFormat) + return STATUS_INVALID_PARAMETER; + + const KSDATAFORMAT_WAVEFORMATEXTENSIBLE *matched = nullptr; + NTSTATUS status = ValidateFormat(m_kind, dataFormat, &matched); + if (!NT_SUCCESS(status)) + return status; + if (!matched) + return STATUS_NO_MATCH; + + FormatFingerprint fingerprint{}; + status = GetFormatFingerprint(matched, &fingerprint); + if (!NT_SUCCESS(status)) + return status; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (m_state != KSSTATE_STOP || m_buffer) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INVALID_DEVICE_STATE; + } + + m_format = *matched; + m_fingerprint = fingerprint; + KeReleaseSpinLock(&m_positionLock, oldIrql); + + if (m_kind == EndpointKind::Render) + status = m_adapter->ConfigureRender(m_fingerprint); + return status; +} + +NTSTATUS CMiniportWaveRTStream::SetState(_In_ KSSTATE state) +{ + if (state < KSSTATE_STOP || state > KSSTATE_RUN) + return STATUS_INVALID_PARAMETER; + + if (state == KSSTATE_RUN) + { + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + if (!clock.Frequency || !clock.QpcFrequency) + return STATUS_DEVICE_HARDWARE_ERROR; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (!m_buffer || !m_bufferSize) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_DEVICE_NOT_READY; + } + + if (m_state == KSSTATE_RUN) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; + } + if (m_eosNotificationSent) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INVALID_DEVICE_STATE; + } + + const bool resetCapture = + m_kind == EndpointKind::Capture && !m_hasRun; + m_clockFrequency = clock.Frequency; + m_qpcFrequency = clock.QpcFrequency; + m_lastClockPosition = clock.Position; + m_lastQpcPosition = clock.QpcPosition; + m_lastPresentationQpc = clock.QpcPosition; + m_state = KSSTATE_RUN; + m_hasRun = true; + KeReleaseSpinLock(&m_positionLock, oldIrql); + + if (resetCapture) + m_adapter->ResetCapture(); + + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (m_state == KSSTATE_RUN) + ArmTimerLocked(); + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; + } + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (m_state == KSSTATE_RUN) + AdvanceLocked(clock); + m_state = state; + + if (state == KSSTATE_STOP) + { + m_clockFrequency = 0; + m_qpcFrequency = 0; + m_lastClockPosition = 0; + m_lastQpcPosition = 0; + m_lastPresentationQpc = 0; + m_frameRemainder = 0; + m_pendingBytes = 0; + m_linearPosition = 0; + m_cyclicPosition = 0; + m_completedPackets = 0; + m_lastReadPacket = ~0ULL; + m_lastWritePacket = MAXULONG; + m_hasRun = false; + m_eosReceived = false; + m_eosNotificationSent = false; + m_eosLinearPosition = 0; + } + KeReleaseSpinLock(&m_positionLock, oldIrql); + + StopTimer(); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::GetPosition( + _Out_ PKSAUDIO_POSITION position) +{ + if (!position) + return STATUS_INVALID_PARAMETER; + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + AdvanceLocked(clock); + ULONG reportedPosition = m_cyclicPosition; + if (m_notificationCount && m_bufferSize) + { + reportedPosition = static_cast( + (reportedPosition + (m_pendingBytes % m_bufferSize)) % + m_bufferSize); + } + position->PlayOffset = reportedPosition; + position->WriteOffset = reportedPosition; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::AllocateAudioBuffer( + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType) +{ + return AllocateBuffer(0, requestedSize, audioBufferMdl, actualSize, + offsetFromFirstPage, cacheType); +} + +void CMiniportWaveRTStream::FreeAudioBuffer( + _In_opt_ PMDL audioBufferMdl, + _In_ ULONG bufferSize) +{ + UNREFERENCED_PARAMETER(bufferSize); + FreeBuffer(audioBufferMdl); +} + +void CMiniportWaveRTStream::GetHWLatency( + _Out_ PKSRTAUDIO_HWLATENCY latency) +{ + if (latency) + RtlZeroMemory(latency, sizeof(*latency)); +} + +NTSTATUS CMiniportWaveRTStream::GetPositionRegister( + _Out_ PKSRTAUDIO_HWREGISTER positionRegister) +{ + if (positionRegister) + RtlZeroMemory(positionRegister, sizeof(*positionRegister)); + return STATUS_NOT_IMPLEMENTED; +} + +NTSTATUS CMiniportWaveRTStream::GetClockRegister( + _Out_ PKSRTAUDIO_HWREGISTER clockRegister) +{ + if (clockRegister) + RtlZeroMemory(clockRegister, sizeof(*clockRegister)); + return STATUS_NOT_IMPLEMENTED; +} + +NTSTATUS CMiniportWaveRTStream::AllocateBufferWithNotification( + _In_ ULONG notificationCount, + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType) +{ + if (notificationCount != 1 && notificationCount != 2) + return STATUS_INVALID_PARAMETER; + + return AllocateBuffer(notificationCount, requestedSize, audioBufferMdl, + actualSize, offsetFromFirstPage, cacheType); +} + +void CMiniportWaveRTStream::FreeBufferWithNotification( + _In_ PMDL audioBufferMdl, + _In_ ULONG bufferSize) +{ + UNREFERENCED_PARAMETER(bufferSize); + FreeBuffer(audioBufferMdl); +} + +NTSTATUS CMiniportWaveRTStream::RegisterNotificationEvent( + _In_ PKEVENT notificationEvent) +{ + if (!notificationEvent) + return STATUS_INVALID_PARAMETER; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + for (ULONG i = 0; i < m_notificationEventCount; ++i) + { + if (m_notificationEvents[i] == notificationEvent) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; + } + } + + if (m_notificationEventCount >= ARRAYSIZE(m_notificationEvents)) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INSUFFICIENT_RESOURCES; + } + + m_notificationEvents[m_notificationEventCount++] = notificationEvent; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::UnregisterNotificationEvent( + _In_ PKEVENT notificationEvent) +{ + if (!notificationEvent) + return STATUS_INVALID_PARAMETER; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + for (ULONG i = 0; i < m_notificationEventCount; ++i) + { + if (m_notificationEvents[i] != notificationEvent) + continue; + + for (ULONG j = i + 1; j < m_notificationEventCount; ++j) + m_notificationEvents[j - 1] = m_notificationEvents[j]; + m_notificationEvents[--m_notificationEventCount] = nullptr; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; + } + + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_NOT_FOUND; +} + +NTSTATUS CMiniportWaveRTStream::GetReadPacket( + _Out_ ULONG *packetNumber, + _Out_ DWORD *flags, + _Out_ ULONG64 *performanceCounterValue, + _Out_ BOOL *moreData) +{ + if (m_kind != EndpointKind::Capture || !packetNumber || !flags || + !performanceCounterValue || !moreData) + return STATUS_INVALID_PARAMETER; + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (!m_notificationCount) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_NOT_SUPPORTED; + } + if (m_state < KSSTATE_PAUSE) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INVALID_DEVICE_STATE; + } + + AdvanceLocked(clock); + + if (!m_completedPackets) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_DEVICE_NOT_READY; + } + + const ULONGLONG availablePacket = m_completedPackets - 1; + if (availablePacket == m_lastReadPacket) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_DEVICE_NOT_READY; + } + + m_lastReadPacket = availablePacket; + *packetNumber = static_cast(availablePacket); + *flags = 0; + + const ULONGLONG blockAlign = + m_format.WaveFormatExt.Format.nBlockAlign; + const ULONGLONG sampleRate = + m_format.WaveFormatExt.Format.nSamplesPerSec; + const ULONGLONG packetStartFrame = + availablePacket * (m_packetSize / blockAlign); + const ULONGLONG currentFrame = m_linearPosition / blockAlign; + const ULONGLONG elapsedFrames = currentFrame - packetStartFrame; + const ULONGLONG elapsedTicks = + elapsedFrames * m_qpcFrequency / sampleRate; + *performanceCounterValue = elapsedTicks < m_lastPresentationQpc ? + m_lastPresentationQpc - elapsedTicks : 0; + *moreData = FALSE; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::SetWritePacket( + _In_ ULONG packetNumber, + _In_ DWORD flags, + _In_ ULONG eosPacketLength) +{ + if (m_kind != EndpointKind::Render) + return STATUS_INVALID_DEVICE_REQUEST; + constexpr DWORD allowedFlags = + KSSTREAM_HEADER_OPTIONSF_ENDOFSTREAM; + if (flags & ~allowedFlags) + return STATUS_INVALID_PARAMETER; + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (!m_notificationCount) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_NOT_SUPPORTED; + } + if (m_eosReceived) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INVALID_DEVICE_STATE; + } + + AdvanceLocked(clock); + ULONGLONG expectedPacket64 = m_completedPackets; + if (m_state == KSSTATE_RUN) + ++expectedPacket64; + const ULONG expectedPacket = static_cast(expectedPacket64); + + const LONG packetDelta = static_cast(packetNumber - expectedPacket); + if (packetDelta < 0) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_DATA_LATE_ERROR; + } + if (packetDelta > 0) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_DATA_OVERRUN; + } + + if (flags & KSSTREAM_HEADER_OPTIONSF_ENDOFSTREAM) + { + const ULONG blockAlign = + m_format.WaveFormatExt.Format.nBlockAlign; + if (eosPacketLength > m_packetSize || + (eosPacketLength % blockAlign) != 0 || + expectedPacket64 > + (MAXULONGLONG - eosPacketLength) / m_packetSize) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INVALID_PARAMETER; + } + + m_eosReceived = true; + m_eosNotificationSent = false; + m_eosLinearPosition = + expectedPacket64 * m_packetSize + eosPacketLength; + } + + m_lastWritePacket = packetNumber; + if (m_eosReceived && m_state == KSSTATE_RUN) + ArmTimerLocked(); + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::GetOutputStreamPresentationPosition( + _Out_ KSAUDIO_PRESENTATION_POSITION *position) +{ + if (m_kind != EndpointKind::Render || !position) + return STATUS_INVALID_PARAMETER; + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (!m_notificationCount) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_NOT_SUPPORTED; + } + + AdvanceLocked(clock); + const ULONGLONG reportedBytes = + m_linearPosition + m_pendingBytes; + position->u64PositionInBlocks = + reportedBytes / m_format.WaveFormatExt.Format.nBlockAlign; + + const ULONG sampleRate = + m_format.WaveFormatExt.Format.nSamplesPerSec; + const ULONGLONG ticksSinceReportedFrame = + sampleRate && clock.Frequency && clock.QpcFrequency ? + (m_frameRemainder * clock.QpcFrequency / clock.Frequency) / + sampleRate : 0; + if (m_pendingBytes || m_state == KSSTATE_RUN) + { + position->u64QPCPosition = + ticksSinceReportedFrame < m_lastQpcPosition ? + m_lastQpcPosition - ticksSinceReportedFrame : 0; + } + else + position->u64QPCPosition = m_lastPresentationQpc; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::GetPacketCount( + _Out_ ULONG *packetCount) +{ + if (m_kind != EndpointKind::Render || !packetCount) + return STATUS_INVALID_PARAMETER; + + const AudioClockSnapshot clock = m_adapter->GetClockSnapshot(); + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (!m_notificationCount) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_NOT_SUPPORTED; + } + + AdvanceLocked(clock); + *packetCount = static_cast(m_completedPackets); + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_SUCCESS; +} + +NTSTATUS CMiniportWaveRTStream::AllocateBuffer( + _In_ ULONG notificationCount, + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType) +{ + if (!audioBufferMdl || !actualSize || !offsetFromFirstPage || !cacheType) + return STATUS_INVALID_PARAMETER; + + *audioBufferMdl = nullptr; + *actualSize = 0; + *offsetFromFirstPage = 0; + *cacheType = MmCached; + + const ULONG blockAlign = m_format.WaveFormatExt.Format.nBlockAlign; + if (!requestedSize || !blockAlign) + return STATUS_INVALID_PARAMETER; + + const ULONG alignment = blockAlign * + (notificationCount ? notificationCount : 1); + const ULONG maximumSize = + m_format.WaveFormatExt.Format.nAvgBytesPerSec * 2; + if (requestedSize > maximumSize || + requestedSize > MAXULONG - (alignment - 1)) + return STATUS_INSUFFICIENT_RESOURCES; + requestedSize = + (requestedSize + alignment - 1) / alignment * alignment; + if (!requestedSize || requestedSize > maximumSize) + return STATUS_INVALID_PARAMETER; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + const bool alreadyAllocated = + m_buffer != nullptr || m_bufferAllocationPending; + if (!alreadyAllocated) + m_bufferAllocationPending = true; + KeReleaseSpinLock(&m_positionLock, oldIrql); + if (alreadyAllocated) + return STATUS_DEVICE_BUSY; + + PHYSICAL_ADDRESS highAddress{}; + highAddress.QuadPart = MAXULONG; + PMDL mdl = m_portStream->AllocatePagesForMdl(highAddress, requestedSize); + if (!mdl) + { + KeAcquireSpinLock(&m_positionLock, &oldIrql); + m_bufferAllocationPending = false; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INSUFFICIENT_RESOURCES; + } + + BYTE *buffer = static_cast( + m_portStream->MapAllocatedPages(mdl, MmCached)); + if (!buffer) + { + m_portStream->FreePagesFromMdl(mdl); + KeAcquireSpinLock(&m_positionLock, &oldIrql); + m_bufferAllocationPending = false; + KeReleaseSpinLock(&m_positionLock, oldIrql); + return STATUS_INSUFFICIENT_RESOURCES; + } + RtlZeroMemory(buffer, requestedSize); + + KeAcquireSpinLock(&m_positionLock, &oldIrql); + m_bufferMdl = mdl; + m_buffer = buffer; + m_bufferAllocationPending = false; + m_bufferSize = requestedSize; + m_notificationCount = notificationCount; + m_packetSize = notificationCount ? + requestedSize / notificationCount : 0; + KeReleaseSpinLock(&m_positionLock, oldIrql); + + *audioBufferMdl = mdl; + *actualSize = requestedSize; + *offsetFromFirstPage = 0; + *cacheType = MmCached; + return STATUS_SUCCESS; +} + +void CMiniportWaveRTStream::FreeBuffer(_In_opt_ PMDL audioBufferMdl) +{ + if (!audioBufferMdl) + return; + + SetState(KSSTATE_STOP); + StopTimer(); + + KIRQL oldIrql; + KeAcquireSpinLock(&m_positionLock, &oldIrql); + if (audioBufferMdl != m_bufferMdl) + { + KeReleaseSpinLock(&m_positionLock, oldIrql); + return; + } + + BYTE *buffer = m_buffer; + PMDL mdl = m_bufferMdl; + m_buffer = nullptr; + m_bufferMdl = nullptr; + m_bufferSize = 0; + m_notificationCount = 0; + m_packetSize = 0; + KeReleaseSpinLock(&m_positionLock, oldIrql); + + if (buffer) + m_portStream->UnmapAllocatedPages(buffer, mdl); + m_portStream->FreePagesFromMdl(mdl); +} + +void CMiniportWaveRTStream::StopTimer() +{ + if (m_timer) + ExCancelTimer(m_timer, nullptr); +} + +void CMiniportWaveRTStream::ArmTimerLocked() +{ + if (!m_timer || m_state != KSSTATE_RUN || !m_notificationCount || + !m_packetSize || !m_clockFrequency) + return; + + const ULONG blockAlign = + m_format.WaveFormatExt.Format.nBlockAlign; + const ULONG sampleRate = + m_format.WaveFormatExt.Format.nSamplesPerSec; + if (!blockAlign || !sampleRate) + return; + + ULONGLONG targetBytes = m_packetSize; + if (m_kind == EndpointKind::Render && m_eosReceived) + { + const ULONGLONG remaining = m_eosLinearPosition > m_linearPosition ? + m_eosLinearPosition - m_linearPosition : 0; + if (remaining < targetBytes) + targetBytes = remaining; + } + + const ULONGLONG bytesUntilTarget = targetBytes > m_pendingBytes ? + targetBytes - m_pendingBytes : 0; + const ULONGLONG framesUntilTarget = + (bytesUntilTarget + blockAlign - 1) / blockAlign; + const ULONGLONG frequency = m_clockFrequency; + + const ULONGLONG frameTicks = framesUntilTarget * frequency; + const ULONGLONG ticksNumerator = frameTicks > m_frameRemainder ? + frameTicks - m_frameRemainder : 0; + ULONGLONG qpcTicks = + (ticksNumerator + sampleRate - 1) / sampleRate; + if (!qpcTicks) + qpcTicks = 1; + + constexpr ULONGLONG kHundredNanosecondsPerSecond = 10000000; + ULONGLONG dueTime = + (qpcTicks * kHundredNanosecondsPerSecond + frequency - 1) / + frequency; + if (!dueTime) + dueTime = 1; + + ExSetTimer(m_timer, -static_cast(dueTime), 0, nullptr); +} + +void CMiniportWaveRTStream::AdvanceLocked( + _In_ const AudioClockSnapshot &clock) +{ + if (m_state != KSSTATE_RUN || !m_buffer || !m_bufferSize || + !clock.Frequency || !clock.QpcFrequency) + return; + + const bool clockDiscontinuity = + !m_clockFrequency || clock.Frequency != m_clockFrequency || + clock.Position < m_lastClockPosition; + if (clockDiscontinuity) + { + m_clockFrequency = clock.Frequency; + m_qpcFrequency = clock.QpcFrequency; + m_lastClockPosition = clock.Position; + m_lastQpcPosition = clock.QpcPosition; + m_lastPresentationQpc = clock.QpcPosition; + m_frameRemainder = 0; + return; + } + + if (clock.Position == m_lastClockPosition) + { + m_qpcFrequency = clock.QpcFrequency; + m_lastQpcPosition = clock.QpcPosition; + return; + } + + const ULONGLONG elapsedTicks = + clock.Position - m_lastClockPosition; + const ULONGLONG numerator = elapsedTicks * + m_format.WaveFormatExt.Format.nSamplesPerSec + m_frameRemainder; + const ULONGLONG frames = numerator / clock.Frequency; + m_frameRemainder = numerator % clock.Frequency; + m_clockFrequency = clock.Frequency; + m_qpcFrequency = clock.QpcFrequency; + m_lastClockPosition = clock.Position; + m_lastQpcPosition = clock.QpcPosition; + + if (frames) + { + const ULONGLONG bytes = frames * + m_format.WaveFormatExt.Format.nBlockAlign; + if (m_notificationCount) + m_pendingBytes += bytes; + else + { + TransferFramesLocked(bytes); + m_lastPresentationQpc = clock.QpcPosition; + } + } + + if (m_notificationCount) + AdvanceEventPacketsLocked(clock); +} + +void CMiniportWaveRTStream::AdvanceEventPacketsLocked( + _In_ const AudioClockSnapshot &clock) +{ + if (!m_packetSize) + return; + + // A WaveRT circular buffer cannot retain more than NotificationCount + // packets. Bound catch-up after a long scheduling or power gap so the DPC + // does not loop over audio that the client has already overwritten. + if (!m_eosReceived) + { + const ULONGLONG readyPackets = m_pendingBytes / m_packetSize; + if (readyPackets > m_notificationCount) + { + const ULONGLONG skippedPackets = + readyPackets - m_notificationCount; + const ULONGLONG skippedBytes = skippedPackets * m_packetSize; + + if (m_kind == EndpointKind::Capture) + m_adapter->DiscardCaptureFrames(m_fingerprint, skippedBytes); + + m_pendingBytes -= skippedBytes; + m_linearPosition += skippedBytes; + m_cyclicPosition = static_cast( + (m_cyclicPosition + (skippedBytes % m_bufferSize)) % + m_bufferSize); + m_completedPackets = m_linearPosition / m_packetSize; + } + } + + while (m_state == KSSTATE_RUN) + { + ULONGLONG transferBytes = m_packetSize; + bool completesEos = false; + + if (m_kind == EndpointKind::Render && m_eosReceived) + { + const ULONGLONG remaining = + m_eosLinearPosition > m_linearPosition ? + m_eosLinearPosition - m_linearPosition : 0; + if (remaining <= transferBytes) + { + transferBytes = remaining; + completesEos = true; + } + } + + if (m_pendingBytes < transferBytes) + break; + + const ULONGLONG oldPacketCount = m_completedPackets; + if (transferBytes) + { + TransferFramesLocked(transferBytes); + m_pendingBytes -= transferBytes; + } + + UpdatePresentationQpcLocked(clock); + m_completedPackets = m_linearPosition / m_packetSize; + + if (m_completedPackets != oldPacketCount || + (completesEos && !m_eosNotificationSent)) + SignalNotificationsLocked(); + + if (completesEos) + { + m_eosNotificationSent = true; + m_state = KSSTATE_PAUSE; + m_pendingBytes = 0; + m_frameRemainder = 0; + break; + } + } +} + +void CMiniportWaveRTStream::UpdatePresentationQpcLocked( + _In_ const AudioClockSnapshot &clock) +{ + const ULONG blockAlign = + m_format.WaveFormatExt.Format.nBlockAlign; + const ULONG sampleRate = + m_format.WaveFormatExt.Format.nSamplesPerSec; + if (!clock.Frequency || !clock.QpcFrequency || !blockAlign || + !sampleRate) + { + m_lastPresentationQpc = clock.QpcPosition; + return; + } + + const ULONGLONG pendingFrames = m_pendingBytes / blockAlign; + const ULONGLONG wholeQpcTicks = + pendingFrames * clock.QpcFrequency / sampleRate; + const ULONGLONG fractionalQpcTicks = + (m_frameRemainder * clock.QpcFrequency / clock.Frequency) / + sampleRate; + const ULONGLONG ticksSinceBoundary = + wholeQpcTicks + fractionalQpcTicks; + m_lastPresentationQpc = ticksSinceBoundary < clock.QpcPosition ? + clock.QpcPosition - ticksSinceBoundary : 0; +} + +void CMiniportWaveRTStream::TransferFramesLocked(_In_ ULONGLONG bytes) +{ + if (!bytes || !m_bufferSize) + return; + + if (m_kind == EndpointKind::Render && m_eosReceived) + { + if (m_linearPosition >= m_eosLinearPosition) + { + m_state = KSSTATE_PAUSE; + return; + } + + const ULONGLONG remaining = + m_eosLinearPosition - m_linearPosition; + if (bytes > remaining) + bytes = remaining; + } + + const ULONG finalPosition = static_cast( + (m_cyclicPosition + (bytes % m_bufferSize)) % m_bufferSize); + ULONG transferBytes = static_cast(MinUlonglong(bytes, + m_bufferSize)); + ULONG offset = bytes > m_bufferSize ? finalPosition : m_cyclicPosition; + const bool renderDataCommitted = + !m_notificationCount || + static_cast(m_lastWritePacket - + static_cast(m_completedPackets)) >= 0; + + if (m_kind == EndpointKind::Capture && bytes > transferBytes) + m_adapter->DiscardCaptureFrames(m_fingerprint, + bytes - transferBytes); + + while (transferBytes) + { + const ULONG untilWrap = m_bufferSize - offset; + const ULONG chunk = transferBytes < untilWrap ? + transferBytes : untilWrap; + + if (m_kind == EndpointKind::Render) + { + // SetWritePacket is issued only after the client finishes writing the + // slot. On an underrun, neither read nor modify a slot that the client + // may still be updating. The capture side zero-fills the resulting + // absence when the render-to-capture FIFO is exhausted. + if (renderDataCommitted) + m_adapter->WriteRenderFrames(m_buffer + offset, chunk); + } + else + { + m_adapter->ReadCaptureFrames(m_fingerprint, + m_buffer + offset, chunk); + } + + transferBytes -= chunk; + offset = (offset + chunk) % m_bufferSize; + } + + m_cyclicPosition = finalPosition; + m_linearPosition += bytes; + + if (m_kind == EndpointKind::Render && m_eosReceived && + m_linearPosition >= m_eosLinearPosition) + m_state = KSSTATE_PAUSE; +} + +void CMiniportWaveRTStream::SignalNotificationsLocked() +{ + for (ULONG i = 0; i < m_notificationEventCount; ++i) + KeSetEvent(m_notificationEvents[i], IO_NO_INCREMENT, FALSE); +} + +void CMiniportWaveRTStream::TimerCallback( + _In_ PEX_TIMER timer, + _In_opt_ void *context) +{ + UNREFERENCED_PARAMETER(timer); + + auto *stream = static_cast(context); + if (!stream) + return; + + const AudioClockSnapshot clock = + stream->m_adapter->GetClockSnapshot(); + KeAcquireSpinLockAtDpcLevel(&stream->m_positionLock); + stream->AdvanceLocked(clock); + if (stream->m_state == KSSTATE_RUN) + stream->ArmTimerLocked(); + KeReleaseSpinLockFromDpcLevel(&stream->m_positionLock); +} diff --git a/idd/LGAudio/MiniportWaveRTStream.h b/idd/LGAudio/MiniportWaveRTStream.h new file mode 100644 index 00000000..6c4c2da5 --- /dev/null +++ b/idd/LGAudio/MiniportWaveRTStream.h @@ -0,0 +1,163 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "Adapter.h" +#include "Common.h" +#include "Formats.h" + +class CMiniportWaveRT; + +class CMiniportWaveRTStream final : + public IMiniportWaveRTStreamNotification, + public IMiniportWaveRTInputStream, + public IMiniportWaveRTOutputStream +{ +public: + CMiniportWaveRTStream( + _In_ CMiniportWaveRT *miniport, + _In_ CAudioAdapter *adapter, + _In_ PPORTWAVERTSTREAM portStream, + _In_ EndpointKind kind); + ~CMiniportWaveRTStream(); + + NTSTATUS Initialize( + _In_ const KSDATAFORMAT_WAVEFORMATEXTENSIBLE *format); + + STDMETHODIMP_(NTSTATUS) QueryInterface( + _In_ REFIID interfaceId, + _COM_Outptr_ void **object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + + STDMETHODIMP_(NTSTATUS) SetFormat( + _In_ PKSDATAFORMAT dataFormat) override; + STDMETHODIMP_(NTSTATUS) SetState(_In_ KSSTATE state) override; + STDMETHODIMP_(NTSTATUS) GetPosition( + _Out_ PKSAUDIO_POSITION position) override; + STDMETHODIMP_(NTSTATUS) AllocateAudioBuffer( + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType) override; + STDMETHODIMP_(void) FreeAudioBuffer( + _In_opt_ PMDL audioBufferMdl, + _In_ ULONG bufferSize) override; + STDMETHODIMP_(void) GetHWLatency( + _Out_ PKSRTAUDIO_HWLATENCY latency) override; + STDMETHODIMP_(NTSTATUS) GetPositionRegister( + _Out_ PKSRTAUDIO_HWREGISTER positionRegister) override; + STDMETHODIMP_(NTSTATUS) GetClockRegister( + _Out_ PKSRTAUDIO_HWREGISTER clockRegister) override; + + STDMETHODIMP_(NTSTATUS) AllocateBufferWithNotification( + _In_ ULONG notificationCount, + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType) override; + STDMETHODIMP_(void) FreeBufferWithNotification( + _In_ PMDL audioBufferMdl, + _In_ ULONG bufferSize) override; + STDMETHODIMP_(NTSTATUS) RegisterNotificationEvent( + _In_ PKEVENT notificationEvent) override; + STDMETHODIMP_(NTSTATUS) UnregisterNotificationEvent( + _In_ PKEVENT notificationEvent) override; + + STDMETHODIMP_(NTSTATUS) GetReadPacket( + _Out_ ULONG *packetNumber, + _Out_ DWORD *flags, + _Out_ ULONG64 *performanceCounterValue, + _Out_ BOOL *moreData) override; + + STDMETHODIMP_(NTSTATUS) SetWritePacket( + _In_ ULONG packetNumber, + _In_ DWORD flags, + _In_ ULONG eosPacketLength) override; + STDMETHODIMP_(NTSTATUS) GetOutputStreamPresentationPosition( + _Out_ KSAUDIO_PRESENTATION_POSITION *position) override; + STDMETHODIMP_(NTSTATUS) GetPacketCount( + _Out_ ULONG *packetCount) override; + +private: + static void TimerCallback( + _In_ PEX_TIMER timer, + _In_opt_ void *context); + + NTSTATUS AllocateBuffer( + _In_ ULONG notificationCount, + _In_ ULONG requestedSize, + _Out_ PMDL *audioBufferMdl, + _Out_ ULONG *actualSize, + _Out_ ULONG *offsetFromFirstPage, + _Out_ MEMORY_CACHING_TYPE *cacheType); + void FreeBuffer(_In_opt_ PMDL audioBufferMdl); + void StopTimer(); + void ArmTimerLocked(); + void AdvanceLocked(_In_ const AudioClockSnapshot &clock); + void AdvanceEventPacketsLocked( + _In_ const AudioClockSnapshot &clock); + void UpdatePresentationQpcLocked( + _In_ const AudioClockSnapshot &clock); + void TransferFramesLocked(_In_ ULONGLONG bytes); + void SignalNotificationsLocked(); + + volatile LONG m_refCount; + CMiniportWaveRT *m_miniport; + CAudioAdapter *m_adapter; + PPORTWAVERTSTREAM m_portStream; + EndpointKind m_kind; + + KSDATAFORMAT_WAVEFORMATEXTENSIBLE m_format; + FormatFingerprint m_fingerprint; + KSSTATE m_state; + + KSPIN_LOCK m_positionLock; + PEX_TIMER m_timer; + bool m_hasRun; + + PMDL m_bufferMdl; + BYTE *m_buffer; + bool m_bufferAllocationPending; + ULONG m_bufferSize; + ULONG m_notificationCount; + ULONG m_packetSize; + PKEVENT m_notificationEvents[8]; + ULONG m_notificationEventCount; + + ULONGLONG m_clockFrequency; + ULONGLONG m_qpcFrequency; + ULONGLONG m_lastClockPosition; + ULONGLONG m_lastQpcPosition; + ULONGLONG m_lastPresentationQpc; + ULONGLONG m_frameRemainder; + ULONGLONG m_pendingBytes; + ULONGLONG m_linearPosition; + ULONG m_cyclicPosition; + ULONGLONG m_completedPackets; + ULONGLONG m_lastReadPacket; + ULONG m_lastWritePacket; + bool m_eosReceived; + bool m_eosNotificationSent; + ULONGLONG m_eosLinearPosition; +}; diff --git a/idd/LGAudio/RingBuffer.cpp b/idd/LGAudio/RingBuffer.cpp new file mode 100644 index 00000000..5abfa5ad --- /dev/null +++ b/idd/LGAudio/RingBuffer.cpp @@ -0,0 +1,323 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "RingBuffer.h" + +namespace LGAudio +{ + namespace + { + constexpr ULONG kRingPoolTag = 'rAGL'; + + ULONG MinUlong(_In_ ULONG left, _In_ ULONG right) + { + return left < right ? left : right; + } + } + + CAudioRingBuffer::CAudioRingBuffer() : + m_buffer(nullptr), + m_capacity(0), + m_readOffset(0), + m_writeOffset(0), + m_available(0), + m_format{}, + m_configured(false) + { + KeInitializeSpinLock(&m_lock); + } + + CAudioRingBuffer::~CAudioRingBuffer() + { + Destroy(); + } + + _IRQL_requires_max_(PASSIVE_LEVEL) + NTSTATUS CAudioRingBuffer::Init() + { + if (m_buffer) + return STATUS_SUCCESS; + + constexpr ULONG capacity = + (kMaximumBytesPerSecond + kMaximumBlockAlign - 1) / + kMaximumBlockAlign * kMaximumBlockAlign; + +#pragma warning(push) +#pragma warning(disable:4996) // Required for pre-2004 Windows 10 releases. + BYTE * buffer = static_cast(ExAllocatePoolWithTag( + NonPagedPoolNx, capacity, kRingPoolTag)); +#pragma warning(pop) + if (!buffer) + return STATUS_INSUFFICIENT_RESOURCES; + + RtlZeroMemory(buffer, capacity); + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + if (!m_buffer) + { + m_buffer = buffer; + m_capacity = capacity; + buffer = nullptr; + } + KeReleaseSpinLock(&m_lock, oldIrql); + + if (buffer) + ExFreePoolWithTag(buffer, kRingPoolTag); + + return STATUS_SUCCESS; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + void CAudioRingBuffer::Destroy() + { + BYTE * buffer; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + buffer = m_buffer; + m_buffer = nullptr; + m_capacity = 0; + m_readOffset = 0; + m_writeOffset = 0; + m_available = 0; + m_format = {}; + m_configured = false; + KeReleaseSpinLock(&m_lock, oldIrql); + + if (buffer) + ExFreePoolWithTag(buffer, kRingPoolTag); + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS CAudioRingBuffer::Configure( + _In_ const FormatFingerprint & format) + { + if (format.sampleRate == 0 || + format.channels == 0 || + format.containerBits == 0 || + (format.containerBits % 8) != 0 || + format.validBits == 0 || + format.validBits > format.containerBits || + format.blockAlign != + format.channels * (format.containerBits / 8)) + return STATUS_INVALID_PARAMETER; + + const ULONGLONG bytesPerSecond = + static_cast(format.sampleRate) * format.blockAlign; + if (bytesPerSecond > kMaximumBytesPerSecond) + return STATUS_NOT_SUPPORTED; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + if (!m_buffer || (m_capacity % format.blockAlign) != 0) + { + KeReleaseSpinLock(&m_lock, oldIrql); + return STATUS_DEVICE_NOT_READY; + } + + m_format = format; + m_readOffset = 0; + m_writeOffset = 0; + m_available = 0; + m_configured = true; + KeReleaseSpinLock(&m_lock, oldIrql); + return STATUS_SUCCESS; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + void CAudioRingBuffer::Reset() + { + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + m_readOffset = 0; + m_writeOffset = 0; + m_available = 0; + KeReleaseSpinLock(&m_lock, oldIrql); + } + + bool CAudioRingBuffer::IsTransferValid(_In_ ULONG bytes) const + { + return + m_buffer && m_configured && m_format.blockAlign != 0 && + (bytes % m_format.blockAlign) == 0; + } + + void CAudioRingBuffer::CopyIntoRing( + _In_reads_bytes_(bytes) const BYTE * data, + _In_ ULONG bytes) + { + const ULONG first = MinUlong(bytes, m_capacity - m_writeOffset); + RtlCopyMemory(m_buffer + m_writeOffset, data, first); + + const ULONG second = bytes - first; + if (second) + RtlCopyMemory(m_buffer, data + first, second); + + m_writeOffset = (m_writeOffset + bytes) % m_capacity; + } + + void CAudioRingBuffer::CopyFromRing( + _Out_writes_bytes_(bytes) BYTE * data, + _In_ ULONG bytes) + { + const ULONG first = MinUlong(bytes, m_capacity - m_readOffset); + RtlCopyMemory(data, m_buffer + m_readOffset, first); + + const ULONG second = bytes - first; + if (second) + RtlCopyMemory(data + first, m_buffer, second); + + m_readOffset = (m_readOffset + bytes) % m_capacity; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG CAudioRingBuffer::Push( + _In_reads_bytes_(bytes) const BYTE * data, + _In_ ULONG bytes, + _Out_opt_ PULONG droppedBytes) + { + const ULONG inputBytes = bytes; + + if (droppedBytes) + *droppedBytes = 0; + if (bytes == 0) + return 0; + if (!data) + return 0; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + if (!IsTransferValid(bytes)) + { + KeReleaseSpinLock(&m_lock, oldIrql); + return 0; + } + + ULONG dropped = 0; + if (bytes >= m_capacity) + { + dropped = m_available + (bytes - m_capacity); + data += bytes - m_capacity; + bytes = m_capacity; + m_readOffset = 0; + m_writeOffset = 0; + m_available = 0; + } + else if (bytes > m_capacity - m_available) + { + dropped = bytes - (m_capacity - m_available); + m_readOffset = (m_readOffset + dropped) % m_capacity; + m_available -= dropped; + } + + CopyIntoRing(data, bytes); + m_available += bytes; + KeReleaseSpinLock(&m_lock, oldIrql); + + if (droppedBytes) + *droppedBytes = dropped; + + // Every source byte was consumed. If input exceeded capacity, the oldest + // part of that input is included in the reported dropped byte count. + return inputBytes; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG CAudioRingBuffer::Pop( + _In_ const FormatFingerprint &format, + _Out_writes_bytes_(bytes) BYTE *data, + _In_ ULONG bytes) + { + if (bytes == 0) + return 0; + if (!data) + return 0; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + if (!IsTransferValid(bytes) || + !EqualFormatFingerprint(m_format, format)) + { + KeReleaseSpinLock(&m_lock, oldIrql); + RtlZeroMemory(data, bytes); + return 0; + } + + const ULONG copied = MinUlong(bytes, m_available); + if (copied) + { + CopyFromRing(data, copied); + m_available -= copied; + } + KeReleaseSpinLock(&m_lock, oldIrql); + + if (copied < bytes) + RtlZeroMemory(data + copied, bytes - copied); + return copied; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG CAudioRingBuffer::Discard( + _In_ const FormatFingerprint &format, + _In_ ULONGLONG bytes) + { + if (!bytes) + return 0; + + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + if (!m_buffer || !m_configured || !m_format.blockAlign || + (bytes % m_format.blockAlign) != 0 || + !EqualFormatFingerprint(m_format, format)) + { + KeReleaseSpinLock(&m_lock, oldIrql); + return 0; + } + + const ULONG discarded = bytes < m_available ? + static_cast(bytes) : m_available; + m_readOffset = (m_readOffset + discarded) % m_capacity; + m_available -= discarded; + KeReleaseSpinLock(&m_lock, oldIrql); + return discarded; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG CAudioRingBuffer::Available() + { + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + const ULONG available = m_available; + KeReleaseSpinLock(&m_lock, oldIrql); + return available; + } + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG CAudioRingBuffer::Capacity() + { + KIRQL oldIrql; + KeAcquireSpinLock(&m_lock, &oldIrql); + const ULONG capacity = m_capacity; + KeReleaseSpinLock(&m_lock, oldIrql); + return capacity; + } + +} diff --git a/idd/LGAudio/RingBuffer.h b/idd/LGAudio/RingBuffer.h new file mode 100644 index 00000000..91ba74bc --- /dev/null +++ b/idd/LGAudio/RingBuffer.h @@ -0,0 +1,103 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "Formats.h" + +namespace LGAudio +{ + /** + * Adapter-owned byte ring used to bridge a render stream to a capture + * stream. The owner must stop its streams before destroying the ring. + */ + class CAudioRingBuffer final + { + public: + CAudioRingBuffer(); + ~CAudioRingBuffer(); + + CAudioRingBuffer(const CAudioRingBuffer &) = delete; + CAudioRingBuffer & operator=(const CAudioRingBuffer &) = delete; + + _IRQL_requires_max_(PASSIVE_LEVEL) + NTSTATUS Init(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + void Destroy(); + + /** Configure a new byte layout and discard any queued audio. */ + _IRQL_requires_max_(DISPATCH_LEVEL) + NTSTATUS Configure(_In_ const FormatFingerprint & format); + + /** Discard queued audio without changing the configured format. */ + _IRQL_requires_max_(DISPATCH_LEVEL) + void Reset(); + + /** + * Consume all input bytes. If necessary, the oldest queued bytes are + * discarded. The return value is bytes consumed from data. + */ + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG Push( + _In_reads_bytes_(bytes) const BYTE * data, + _In_ ULONG bytes, + _Out_opt_ PULONG droppedBytes = nullptr); + + /** + * Pop into data and zero-fill an underrun. The return value is the number + * of bytes obtained from the ring, excluding zero-fill. + */ + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG Pop( + _In_ const FormatFingerprint &format, + _Out_writes_bytes_(bytes) BYTE *data, + _In_ ULONG bytes); + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG Discard( + _In_ const FormatFingerprint &format, + _In_ ULONGLONG bytes); + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG Available(); + + _IRQL_requires_max_(DISPATCH_LEVEL) + ULONG Capacity(); + + private: + bool IsTransferValid(_In_ ULONG bytes) const; + void CopyIntoRing(_In_reads_bytes_(bytes) const BYTE * data, + _In_ ULONG bytes); + void CopyFromRing(_Out_writes_bytes_(bytes) BYTE * data, + _In_ ULONG bytes); + + KSPIN_LOCK m_lock; + BYTE * m_buffer; + ULONG m_capacity; + ULONG m_readOffset; + ULONG m_writeOffset; + ULONG m_available; + FormatFingerprint m_format; + bool m_configured; + }; +} + +using CAudioRingBuffer = LGAudio::CAudioRingBuffer; diff --git a/idd/LGIdd.sln b/idd/LGIdd.sln index 229856b8..457f4548 100644 --- a/idd/LGIdd.sln +++ b/idd/LGIdd.sln @@ -12,6 +12,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LGIdd", "LGIdd\LGIdd.vcxpro EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LGInput", "LGInput\LGInput.vcxproj", "{2477B25B-CB62-4AD9-A260-CE5F00D77EEB}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LGAudio", "LGAudio\LGAudio.vcxproj", "{02146E7A-FA7C-4071-8759-8B9A89452367}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LGMP", "..\repos\LGMP\LGMP.vcxproj", "{FFCC376C-4D98-4B50-B431-E1BBC9C67E65}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LGIddHelper", "LGIddHelper\LGIddHelper.vcxproj", "{0045D7AD-3F26-4B87-81CB-78D18839596D}" @@ -56,6 +58,16 @@ Global {2477B25B-CB62-4AD9-A260-CE5F00D77EEB}.Release|x64.Build.0 = Release|x64 {2477B25B-CB62-4AD9-A260-CE5F00D77EEB}.Release-LGInput|x64.ActiveCfg = Release|x64 {2477B25B-CB62-4AD9-A260-CE5F00D77EEB}.Release-LGInput|x64.Build.0 = Release|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Debug|x64.ActiveCfg = Debug|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Debug|x64.Build.0 = Debug|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Debug|x64.Deploy.0 = Debug|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Debug-LGInput|x64.ActiveCfg = Debug|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Debug-LGInput|x64.Build.0 = Debug|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Release|x64.ActiveCfg = Release|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Release|x64.Build.0 = Release|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Release|x64.Deploy.0 = Release|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Release-LGInput|x64.ActiveCfg = Release|x64 + {02146E7A-FA7C-4071-8759-8B9A89452367}.Release-LGInput|x64.Build.0 = Release|x64 {FFCC376C-4D98-4B50-B431-E1BBC9C67E65}.Debug|x64.ActiveCfg = Debug|x64 {FFCC376C-4D98-4B50-B431-E1BBC9C67E65}.Debug|x64.Build.0 = Debug|x64 {FFCC376C-4D98-4B50-B431-E1BBC9C67E65}.Debug-LGInput|x64.ActiveCfg = Debug|x64