From c261fa053f4dce76d0852bdf749ff573ed99126b Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 1 Sep 2026 22:42:05 +1000 Subject: [PATCH] [idd] audio: add mediated broker control endpoint Expose a private, versioned control device that lets the IDD discover and claim the active audio adapter without using the public PortCls endpoints. Restrict access to SYSTEM and UMDF hosts, track adapter and binding generations, and keep broker failure optional. Reserve data-plane capabilities without advertising them until PCM queues are available. --- idd/LGAudio/Adapter.cpp | 12 + idd/LGAudio/BrokerDevice.cpp | 925 ++++++++++++++++++++++++++++ idd/LGAudio/BrokerDevice.h | 77 +++ idd/LGAudio/Driver.cpp | 79 ++- idd/LGAudio/LGAudio.vcxproj | 7 +- idd/LGAudio/LGAudio.vcxproj.filters | 5 + idd/LGCommon/LGAudioBroker.h | 181 ++++++ 7 files changed, 1281 insertions(+), 5 deletions(-) create mode 100644 idd/LGAudio/BrokerDevice.cpp create mode 100644 idd/LGAudio/BrokerDevice.h create mode 100644 idd/LGCommon/LGAudioBroker.h diff --git a/idd/LGAudio/Adapter.cpp b/idd/LGAudio/Adapter.cpp index 6ccd20ca..225cef1c 100644 --- a/idd/LGAudio/Adapter.cpp +++ b/idd/LGAudio/Adapter.cpp @@ -19,6 +19,7 @@ */ #include "Adapter.h" +#include "BrokerDevice.h" #include "Formats.h" #include "MiniportTopology.h" #include "MiniportWaveRT.h" @@ -237,6 +238,17 @@ NTSTATUS StartAudioDevice( status = PcRegisterPhysicalConnection(deviceObject, topologyCapture, kCaptureTopologyBridgePin, waveCapture, kCaptureWaveBridgePin); + if (NT_SUCCESS(status)) + { + const NTSTATUS brokerStatus = + BrokerDeviceAttachAdapter(deviceObject, adapter); + if (!NT_SUCCESS(brokerStatus)) + { + LGAUDIO_TRACE_ERROR("broker adapter unavailable: 0x%08X", + brokerStatus); + } + } + SafeRelease(waveCapture); SafeRelease(topologyCapture); SafeRelease(waveRender); diff --git a/idd/LGAudio/BrokerDevice.cpp b/idd/LGAudio/BrokerDevice.cpp new file mode 100644 index 00000000..3ef147c3 --- /dev/null +++ b/idd/LGAudio/BrokerDevice.cpp @@ -0,0 +1,925 @@ +/** + * 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 "BrokerDevice.h" + +#include "Adapter.h" +#include "LGAudioBroker.h" + +extern "C" +{ +#include +} + +namespace +{ + constexpr ULONG kBrokerFileSignature = 'fBGL'; + + struct BrokerFileContext + { + ULONG signature; + bool bound; + UCHAR reserved[3]; + ULONGLONG adapterGeneration; + ULONGLONG bindingGeneration; + }; + + struct BrokerControlExtension + { + KSPIN_LOCK stateLock; + PFILE_OBJECT owner; + BrokerFileContext file; + }; + + struct AdapterSnapshot + { + bool present; + ULONGLONG generation; + ULONGLONG eventSequence; + }; + + FAST_MUTEX g_controlLifecycleLock; + FAST_MUTEX g_adapterLifecycleLock; + KSPIN_LOCK g_adapterStateLock; + EX_RUNDOWN_REF g_adapterRundown; + + PDRIVER_OBJECT g_driverObject; + PDEVICE_OBJECT g_controlDevice; + CAudioAdapter *g_adapter; + PDEVICE_OBJECT g_adapterDevice; + + volatile LONG g_initialized; + volatile LONG g_retiring; + volatile LONG g_pnpDeviceCount; + bool g_linkCreated; + bool g_rundownCompleted; + + volatile LONG64 g_adapterGeneration; + volatile LONG64 g_eventSequence; + volatile LONG64 g_bindingGeneration; + volatile LONG64 g_bindCount; + volatile LONG64 g_unbindCount; + volatile LONG64 g_cleanupCount; + volatile LONG64 g_rejectedRequestCount; + + BrokerControlExtension *GetControlExtension( + _In_ PDEVICE_OBJECT deviceObject) + { + return static_cast( + deviceObject->DeviceExtension); + } + + ULONGLONG ReadCounter(_In_ volatile LONG64 *counter) + { + return static_cast( + InterlockedCompareExchange64(counter, 0, 0)); + } + + void IncrementEventSequence() + { + InterlockedIncrement64(&g_eventSequence); + } + + PDEVICE_OBJECT LoadControlDevice() + { + return static_cast( + InterlockedCompareExchangePointer( + reinterpret_cast(&g_controlDevice), + nullptr, nullptr)); + } + + PDEVICE_OBJECT ExchangeControlDevice( + _In_opt_ PDEVICE_OBJECT deviceObject) + { + return static_cast(InterlockedExchangePointer( + reinterpret_cast(&g_controlDevice), + deviceObject)); + } + + NTSTATUS CompleteIrp( + _Inout_ PIRP irp, + _In_ NTSTATUS status, + _In_ ULONG_PTR information = 0) + { + irp->IoStatus.Status = status; + irp->IoStatus.Information = information; + IoCompleteRequest(irp, IO_NO_INCREMENT); + return status; + } + + AdapterSnapshot GetAdapterSnapshot() + { + AdapterSnapshot snapshot{}; + + KIRQL oldIrql; + KeAcquireSpinLock(&g_adapterStateLock, &oldIrql); + snapshot.present = g_adapter != nullptr; + snapshot.generation = ReadCounter(&g_adapterGeneration); + snapshot.eventSequence = ReadCounter(&g_eventSequence); + KeReleaseSpinLock(&g_adapterStateLock, oldIrql); + return snapshot; + } + + bool IsOwnedFile( + _In_ BrokerControlExtension *extension, + _In_opt_ PFILE_OBJECT fileObject) + { + return fileObject && extension->owner == fileObject && + fileObject->FsContext == &extension->file && + extension->file.signature == kBrokerFileSignature; + } + + bool UnbindFileLocked(_In_ BrokerControlExtension *extension) + { + if (!extension->file.bound) + return false; + + extension->file.bound = false; + extension->file.adapterGeneration = 0; + extension->file.bindingGeneration = 0; + InterlockedIncrement64(&g_bindingGeneration); + return true; + } + + _IRQL_requires_max_(PASSIVE_LEVEL) + void InvalidateControlBinding() + { + ExAcquireFastMutex(&g_controlLifecycleLock); + PDEVICE_OBJECT controlDevice = LoadControlDevice(); + if (!controlDevice) + { + ExReleaseFastMutex(&g_controlLifecycleLock); + return; + } + + BrokerControlExtension *extension = + GetControlExtension(controlDevice); + bool unbound; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + unbound = UnbindFileLocked(extension); + KeReleaseSpinLock(&extension->stateLock, oldIrql); + ExReleaseFastMutex(&g_controlLifecycleLock); + + if (unbound) + { + InterlockedIncrement64(&g_unbindCount); + IncrementEventSequence(); + } + } + + void DeleteSymbolicLinkLocked() + { + if (!g_linkCreated) + return; + + UNICODE_STRING dosName = + RTL_CONSTANT_STRING(LGAUDIO_BROKER_DOS_DEVICE_NAME); + IoDeleteSymbolicLink(&dosName); + g_linkCreated = false; + } + + NTSTATUS CreateControlDeviceLocked() + { + if (LoadControlDevice()) + return STATUS_SUCCESS; + if (!g_driverObject) + return STATUS_INVALID_DEVICE_STATE; + + UNICODE_STRING ntName = + RTL_CONSTANT_STRING(LGAUDIO_BROKER_NT_DEVICE_NAME); + UNICODE_STRING dosName = + RTL_CONSTANT_STRING(LGAUDIO_BROKER_DOS_DEVICE_NAME); + UNICODE_STRING security = RTL_CONSTANT_STRING( + L"D:P(A;;GA;;;SY)(A;;GA;;;UD)"); + + PDEVICE_OBJECT deviceObject = nullptr; + NTSTATUS status = IoCreateDeviceSecure( + g_driverObject, + sizeof(BrokerControlExtension), + &ntName, + FILE_DEVICE_UNKNOWN, + FILE_DEVICE_SECURE_OPEN, + FALSE, + &security, + &LGAUDIO_BROKER_DEVICE_CLASS_GUID, + &deviceObject); + if (!NT_SUCCESS(status)) + return status; + + BrokerControlExtension *extension = + GetControlExtension(deviceObject); + RtlZeroMemory(extension, sizeof(*extension)); + KeInitializeSpinLock(&extension->stateLock); + + deviceObject->Flags |= DO_DIRECT_IO; + + InterlockedExchange(&g_retiring, 0); + const PDEVICE_OBJECT previous = static_cast( + InterlockedCompareExchangePointer( + reinterpret_cast(&g_controlDevice), + deviceObject, nullptr)); + if (previous) + { + IoDeleteDevice(deviceObject); + return STATUS_DEVICE_BUSY; + } + + status = IoCreateSymbolicLink(&dosName, &ntName); + if (!NT_SUCCESS(status)) + { + const PDEVICE_OBJECT removed = ExchangeControlDevice(nullptr); + NT_ASSERT(removed == deviceObject); + UNREFERENCED_PARAMETER(removed); + IoDeleteDevice(deviceObject); + return status; + } + + g_linkCreated = true; + deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; + return STATUS_SUCCESS; + } + + void SetControlRetiringLocked(_In_ bool retiring) + { + PDEVICE_OBJECT controlDevice = LoadControlDevice(); + if (!controlDevice) + return; + + BrokerControlExtension *extension = + GetControlExtension(controlDevice); + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + InterlockedExchange(&g_retiring, retiring ? 1 : 0); + KeReleaseSpinLock(&extension->stateLock, oldIrql); + } + + PDEVICE_OBJECT TakeRetiredControlDeviceLocked() + { + PDEVICE_OBJECT controlDevice = LoadControlDevice(); + if (!controlDevice) + return nullptr; + + BrokerControlExtension *extension = + GetControlExtension(controlDevice); + bool takeDevice = false; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (InterlockedCompareExchange(&g_retiring, 0, 0) != 0 && + !extension->owner) + { + const PDEVICE_OBJECT removed = ExchangeControlDevice(nullptr); + NT_ASSERT(removed == controlDevice); + takeDevice = removed == controlDevice; + } + KeReleaseSpinLock(&extension->stateLock, oldIrql); + return takeDevice ? controlDevice : nullptr; + } + + NTSTATUS DispatchCreate( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) + { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + PFILE_OBJECT fileObject = stack->FileObject; + if (!fileObject || fileObject->RelatedFileObject || + fileObject->FileName.Length != 0) + { + InterlockedIncrement64(&g_rejectedRequestCount); + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + } + + BrokerControlExtension *extension = + GetControlExtension(deviceObject); + NTSTATUS status = STATUS_SUCCESS; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (InterlockedCompareExchange(&g_retiring, 0, 0) != 0) + status = STATUS_DELETE_PENDING; + else if (extension->owner) + status = STATUS_SHARING_VIOLATION; + else + { + extension->owner = fileObject; + extension->file.signature = kBrokerFileSignature; + extension->file.bound = false; + extension->file.adapterGeneration = 0; + extension->file.bindingGeneration = 0; + fileObject->FsContext = &extension->file; + } + KeReleaseSpinLock(&extension->stateLock, oldIrql); + + if (!NT_SUCCESS(status)) + InterlockedIncrement64(&g_rejectedRequestCount); + return CompleteIrp(irp, status); + } + + NTSTATUS DispatchCleanup( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) + { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + BrokerControlExtension *extension = + GetControlExtension(deviceObject); + bool unbound = false; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (IsOwnedFile(extension, stack->FileObject)) + unbound = UnbindFileLocked(extension); + KeReleaseSpinLock(&extension->stateLock, oldIrql); + + InterlockedIncrement64(&g_cleanupCount); + if (unbound) + { + InterlockedIncrement64(&g_unbindCount); + IncrementEventSequence(); + } + return CompleteIrp(irp, STATUS_SUCCESS); + } + + NTSTATUS DispatchClose( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) + { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + PFILE_OBJECT fileObject = stack->FileObject; + BrokerControlExtension *extension = + GetControlExtension(deviceObject); + bool unbound = false; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (IsOwnedFile(extension, fileObject)) + { + unbound = UnbindFileLocked(extension); + extension->owner = nullptr; + extension->file.signature = 0; + if (fileObject) + fileObject->FsContext = nullptr; + } + KeReleaseSpinLock(&extension->stateLock, oldIrql); + + if (unbound) + { + InterlockedIncrement64(&g_unbindCount); + IncrementEventSequence(); + } + + const NTSTATUS status = CompleteIrp(irp, STATUS_SUCCESS); + + PDEVICE_OBJECT deleteDevice = nullptr; + ExAcquireFastMutex(&g_controlLifecycleLock); + deleteDevice = TakeRetiredControlDeviceLocked(); + ExReleaseFastMutex(&g_controlLifecycleLock); + if (deleteDevice) + IoDeleteDevice(deleteDevice); + + return status; + } + + bool ValidateVersionedInput( + _In_reads_bytes_(inputLength) const void *input, + _In_ ULONG inputLength, + _In_ ULONG requiredSize) + { + if (!input || inputLength < sizeof(LGAudioBrokerHeader)) + return false; + + const auto *header = + static_cast(input); + return header->size >= requiredSize && + header->size <= inputLength && + header->version == LGAUDIO_BROKER_ABI_VERSION; + } + + NTSTATUS QueryCaps( + _Inout_ PIRP irp, + _In_ ULONG outputLength) + { + if (outputLength < sizeof(LGAudioBrokerCaps)) + return CompleteIrp(irp, STATUS_BUFFER_TOO_SMALL, + sizeof(LGAudioBrokerCaps)); + + auto *caps = static_cast( + irp->AssociatedIrp.SystemBuffer); + RtlZeroMemory(caps, sizeof(*caps)); + caps->size = sizeof(*caps); + caps->version = LGAUDIO_BROKER_ABI_VERSION; + caps->minimumVersion = LGAUDIO_BROKER_ABI_VERSION; + caps->maximumVersion = LGAUDIO_BROKER_ABI_VERSION; + caps->capabilities = LGAUDIO_BROKER_CAP_CONTROL; + return CompleteIrp(irp, STATUS_SUCCESS, sizeof(*caps)); + } + + NTSTATUS QueryState( + _In_ BrokerControlExtension *extension, + _In_ PFILE_OBJECT fileObject, + _Inout_ PIRP irp, + _In_ ULONG outputLength) + { + if (outputLength < sizeof(LGAudioBrokerState)) + return CompleteIrp(irp, STATUS_BUFFER_TOO_SMALL, + sizeof(LGAudioBrokerState)); + + const AdapterSnapshot snapshot = GetAdapterSnapshot(); + bool bound = false; + ULONGLONG bindingGeneration; + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + bindingGeneration = ReadCounter(&g_bindingGeneration); + if (IsOwnedFile(extension, fileObject)) + { + bound = extension->file.bound && snapshot.present && + extension->file.adapterGeneration == snapshot.generation; + } + KeReleaseSpinLock(&extension->stateLock, oldIrql); + + auto *state = static_cast( + irp->AssociatedIrp.SystemBuffer); + RtlZeroMemory(state, sizeof(*state)); + state->size = sizeof(*state); + state->version = LGAUDIO_BROKER_ABI_VERSION; + state->adapterGeneration = snapshot.generation; + state->eventSequence = snapshot.eventSequence; + state->bindingGeneration = bindingGeneration; + state->render.state = LGAUDIO_BROKER_STREAM_UNAVAILABLE; + state->capture.state = LGAUDIO_BROKER_STREAM_UNAVAILABLE; + if (snapshot.present) + state->flags |= LGAUDIO_BROKER_STATE_ADAPTER_PRESENT; + if (bound) + state->flags |= LGAUDIO_BROKER_STATE_BOUND; + + return CompleteIrp(irp, STATUS_SUCCESS, sizeof(*state)); + } + + NTSTATUS Bind( + _In_ BrokerControlExtension *extension, + _In_ PFILE_OBJECT fileObject, + _Inout_ PIRP irp, + _In_ ULONG inputLength, + _In_ ULONG outputLength) + { + const void *input = irp->AssociatedIrp.SystemBuffer; + if (!ValidateVersionedInput(input, inputLength, + sizeof(LGAudioBrokerBind))) + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + + if (outputLength < sizeof(LGAudioBrokerBind)) + return CompleteIrp(irp, STATUS_BUFFER_TOO_SMALL, + sizeof(LGAudioBrokerBind)); + + auto *bind = static_cast( + irp->AssociatedIrp.SystemBuffer); + if (bind->flags || bind->reserved0 || bind->reserved[0] || + bind->reserved[1] || bind->bindingGeneration) + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + + ULONGLONG generation = 0; + CAudioAdapter *adapter = BrokerDeviceAcquireAdapter(&generation); + if (!adapter) + return CompleteIrp(irp, STATUS_DEVICE_NOT_READY); + + NTSTATUS status = STATUS_SUCCESS; + if (bind->adapterGeneration != generation) + status = STATUS_RETRY; + else + { + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (!IsOwnedFile(extension, fileObject)) + status = STATUS_INVALID_HANDLE; + else if (extension->file.bound) + status = STATUS_INVALID_DEVICE_STATE; + else + { + const ULONGLONG bindingGeneration = static_cast( + InterlockedIncrement64(&g_bindingGeneration)); + extension->file.bound = true; + extension->file.adapterGeneration = generation; + extension->file.bindingGeneration = bindingGeneration; + bind->bindingGeneration = bindingGeneration; + } + KeReleaseSpinLock(&extension->stateLock, oldIrql); + } + + BrokerDeviceReleaseAdapter(); + if (NT_SUCCESS(status)) + { + InterlockedIncrement64(&g_bindCount); + IncrementEventSequence(); + } + return CompleteIrp(irp, status, + NT_SUCCESS(status) ? sizeof(*bind) : 0); + } + + NTSTATUS Unbind( + _In_ BrokerControlExtension *extension, + _In_ PFILE_OBJECT fileObject, + _Inout_ PIRP irp, + _In_ ULONG inputLength) + { + const void *input = irp->AssociatedIrp.SystemBuffer; + if (!ValidateVersionedInput(input, inputLength, + sizeof(LGAudioBrokerHeader))) + return CompleteIrp(irp, STATUS_INVALID_PARAMETER); + + bool unbound = false; + NTSTATUS status = STATUS_SUCCESS; + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + if (!IsOwnedFile(extension, fileObject)) + status = STATUS_INVALID_HANDLE; + else + unbound = UnbindFileLocked(extension); + KeReleaseSpinLock(&extension->stateLock, oldIrql); + + if (unbound) + { + InterlockedIncrement64(&g_unbindCount); + IncrementEventSequence(); + } + return CompleteIrp(irp, status); + } + + NTSTATUS QueryStats( + _Inout_ PIRP irp, + _In_ ULONG outputLength) + { + if (outputLength < sizeof(LGAudioBrokerStats)) + return CompleteIrp(irp, STATUS_BUFFER_TOO_SMALL, + sizeof(LGAudioBrokerStats)); + + auto *stats = static_cast( + irp->AssociatedIrp.SystemBuffer); + RtlZeroMemory(stats, sizeof(*stats)); + stats->size = sizeof(*stats); + stats->version = LGAUDIO_BROKER_ABI_VERSION; + stats->adapterGeneration = ReadCounter(&g_adapterGeneration); + stats->eventSequence = ReadCounter(&g_eventSequence); + stats->bindingGeneration = ReadCounter(&g_bindingGeneration); + stats->bindCount = ReadCounter(&g_bindCount); + stats->unbindCount = ReadCounter(&g_unbindCount); + stats->cleanupCount = ReadCounter(&g_cleanupCount); + stats->rejectedRequestCount = + ReadCounter(&g_rejectedRequestCount); + return CompleteIrp(irp, STATUS_SUCCESS, sizeof(*stats)); + } + + NTSTATUS DispatchDeviceControl( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) + { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + PFILE_OBJECT fileObject = stack->FileObject; + BrokerControlExtension *extension = + GetControlExtension(deviceObject); + + KIRQL oldIrql; + KeAcquireSpinLock(&extension->stateLock, &oldIrql); + const bool owner = IsOwnedFile(extension, fileObject); + KeReleaseSpinLock(&extension->stateLock, oldIrql); + if (!owner) + { + InterlockedIncrement64(&g_rejectedRequestCount); + return CompleteIrp(irp, STATUS_INVALID_HANDLE); + } + + const ULONG inputLength = + stack->Parameters.DeviceIoControl.InputBufferLength; + const ULONG outputLength = + stack->Parameters.DeviceIoControl.OutputBufferLength; + NTSTATUS status; + + switch (stack->Parameters.DeviceIoControl.IoControlCode) + { + case IOCTL_LGAUDIO_BROKER_QUERY_CAPS: + status = QueryCaps(irp, outputLength); + break; + + case IOCTL_LGAUDIO_BROKER_QUERY_STATE: + status = QueryState(extension, fileObject, irp, outputLength); + break; + + case IOCTL_LGAUDIO_BROKER_BIND: + status = Bind(extension, fileObject, irp, inputLength, + outputLength); + break; + + case IOCTL_LGAUDIO_BROKER_UNBIND: + status = Unbind(extension, fileObject, irp, inputLength); + break; + + case IOCTL_LGAUDIO_BROKER_QUERY_STATS: + status = QueryStats(irp, outputLength); + break; + + default: + status = CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + break; + } + + if (!NT_SUCCESS(status)) + InterlockedIncrement64(&g_rejectedRequestCount); + return status; + } +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS BrokerDeviceInitialize(_In_ PDRIVER_OBJECT driverObject) +{ + if (!driverObject) + return STATUS_INVALID_PARAMETER; + + if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0) + return STATUS_SUCCESS; + + ExInitializeFastMutex(&g_controlLifecycleLock); + ExInitializeFastMutex(&g_adapterLifecycleLock); + KeInitializeSpinLock(&g_adapterStateLock); + ExInitializeRundownProtection(&g_adapterRundown); + g_driverObject = driverObject; + + ExAcquireFastMutex(&g_controlLifecycleLock); + const NTSTATUS status = CreateControlDeviceLocked(); + ExReleaseFastMutex(&g_controlLifecycleLock); + return status; +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceShutdown() +{ + if (InterlockedCompareExchange(&g_initialized, 0, 1) != 1) + return; + + InvalidateControlBinding(); + + ExAcquireFastMutex(&g_adapterLifecycleLock); + bool waitForRundown = false; + KIRQL oldIrql; + KeAcquireSpinLock(&g_adapterStateLock, &oldIrql); + if (g_adapter) + { + g_adapter = nullptr; + g_adapterDevice = nullptr; + InterlockedIncrement64(&g_adapterGeneration); + IncrementEventSequence(); + waitForRundown = true; + } + KeReleaseSpinLock(&g_adapterStateLock, oldIrql); + if (waitForRundown) + { + ExWaitForRundownProtectionRelease(&g_adapterRundown); + g_rundownCompleted = true; + } + ExReleaseFastMutex(&g_adapterLifecycleLock); + + PDEVICE_OBJECT deleteDevice = nullptr; + ExAcquireFastMutex(&g_controlLifecycleLock); + SetControlRetiringLocked(true); + DeleteSymbolicLinkLocked(); + deleteDevice = TakeRetiredControlDeviceLocked(); + NT_ASSERT(deleteDevice || !LoadControlDevice()); + ExReleaseFastMutex(&g_controlLifecycleLock); + + if (deleteDevice) + IoDeleteDevice(deleteDevice); + g_driverObject = nullptr; +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +bool BrokerDeviceIsControlDevice(_In_ PDEVICE_OBJECT deviceObject) +{ + return deviceObject && deviceObject == LoadControlDevice(); +} + +_IRQL_requires_max_(APC_LEVEL) +NTSTATUS BrokerDeviceDispatch( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) +{ + if (!deviceObject || !irp) + return STATUS_INVALID_PARAMETER; + + const UCHAR major = IoGetCurrentIrpStackLocation(irp)->MajorFunction; + switch (major) + { + case IRP_MJ_CREATE: + return DispatchCreate(deviceObject, irp); + + case IRP_MJ_CLEANUP: + return DispatchCleanup(deviceObject, irp); + + case IRP_MJ_CLOSE: + return DispatchClose(deviceObject, irp); + + case IRP_MJ_DEVICE_CONTROL: + return DispatchDeviceControl(deviceObject, irp); + + case IRP_MJ_FLUSH_BUFFERS: + return CompleteIrp(irp, STATUS_SUCCESS); + + case IRP_MJ_READ: + case IRP_MJ_WRITE: + InterlockedIncrement64(&g_rejectedRequestCount); + return CompleteIrp(irp, STATUS_NOT_SUPPORTED); + + default: + InterlockedIncrement64(&g_rejectedRequestCount); + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceNotifyPnpDeviceAdded() +{ + const LONG count = InterlockedIncrement(&g_pnpDeviceCount); + if (count != 1 || !g_initialized) + return; + + ExAcquireFastMutex(&g_controlLifecycleLock); + if (!LoadControlDevice()) + { + const NTSTATUS status = CreateControlDeviceLocked(); + if (!NT_SUCCESS(status)) + { + LGAUDIO_TRACE_ERROR("broker control device unavailable: 0x%08X", + status); + } + } + else if (InterlockedCompareExchange(&g_retiring, 0, 0) != 0) + { + UNICODE_STRING ntName = + RTL_CONSTANT_STRING(LGAUDIO_BROKER_NT_DEVICE_NAME); + UNICODE_STRING dosName = + RTL_CONSTANT_STRING(LGAUDIO_BROKER_DOS_DEVICE_NAME); + const NTSTATUS status = IoCreateSymbolicLink(&dosName, &ntName); + if (NT_SUCCESS(status)) + { + g_linkCreated = true; + SetControlRetiringLocked(false); + } + else + { + LGAUDIO_TRACE_ERROR("broker link unavailable: 0x%08X", status); + } + } + ExReleaseFastMutex(&g_controlLifecycleLock); +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceNotifyPnpDeviceRemoved() +{ + LONG count = InterlockedDecrement(&g_pnpDeviceCount); + if (count > 0) + return; + if (count < 0) + InterlockedExchange(&g_pnpDeviceCount, 0); + + InvalidateControlBinding(); + + PDEVICE_OBJECT deleteDevice = nullptr; + ExAcquireFastMutex(&g_controlLifecycleLock); + SetControlRetiringLocked(true); + DeleteSymbolicLinkLocked(); + deleteDevice = TakeRetiredControlDeviceLocked(); + ExReleaseFastMutex(&g_controlLifecycleLock); + + if (deleteDevice) + IoDeleteDevice(deleteDevice); +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS BrokerDeviceAttachAdapter( + _In_ PDEVICE_OBJECT deviceObject, + _In_ CAudioAdapter *adapter) +{ + if (!deviceObject || !adapter || !g_initialized) + return STATUS_INVALID_PARAMETER; + + LGAudioPortClsDeviceContext *deviceContext = + GetLGAudioDeviceContext(deviceObject); + adapter->AddRef(); + if (InterlockedCompareExchangePointer(&deviceContext->adapter, + adapter, nullptr) != nullptr) + { + adapter->Release(); + return STATUS_DEVICE_BUSY; + } + + NTSTATUS status = STATUS_SUCCESS; + ExAcquireFastMutex(&g_adapterLifecycleLock); + if (g_adapter) + status = STATUS_DEVICE_BUSY; + else + { + if (g_rundownCompleted) + { + ExReInitializeRundownProtection(&g_adapterRundown); + g_rundownCompleted = false; + } + + KIRQL oldIrql; + KeAcquireSpinLock(&g_adapterStateLock, &oldIrql); + g_adapter = adapter; + g_adapterDevice = deviceObject; + InterlockedIncrement64(&g_adapterGeneration); + IncrementEventSequence(); + KeReleaseSpinLock(&g_adapterStateLock, oldIrql); + } + ExReleaseFastMutex(&g_adapterLifecycleLock); + + if (!NT_SUCCESS(status)) + { + InterlockedExchangePointer(&deviceContext->adapter, nullptr); + adapter->Release(); + } + return status; +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceDetachAdapter(_In_ PDEVICE_OBJECT deviceObject) +{ + if (!deviceObject || !g_initialized) + return; + + LGAudioPortClsDeviceContext *deviceContext = + GetLGAudioDeviceContext(deviceObject); + auto *adapter = static_cast( + InterlockedExchangePointer(&deviceContext->adapter, nullptr)); + if (!adapter) + return; + + bool detached = false; + ExAcquireFastMutex(&g_adapterLifecycleLock); + KIRQL oldIrql; + KeAcquireSpinLock(&g_adapterStateLock, &oldIrql); + if (g_adapter == adapter && g_adapterDevice == deviceObject) + { + g_adapter = nullptr; + g_adapterDevice = nullptr; + InterlockedIncrement64(&g_adapterGeneration); + IncrementEventSequence(); + detached = true; + } + KeReleaseSpinLock(&g_adapterStateLock, oldIrql); + + if (detached) + { + ExWaitForRundownProtectionRelease(&g_adapterRundown); + g_rundownCompleted = true; + InvalidateControlBinding(); + } + ExReleaseFastMutex(&g_adapterLifecycleLock); + adapter->Release(); +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +CAudioAdapter *BrokerDeviceAcquireAdapter( + _Out_opt_ PULONGLONG generation) +{ + if (generation) + *generation = 0; + if (!g_initialized || + !ExAcquireRundownProtection(&g_adapterRundown)) + return nullptr; + + CAudioAdapter *adapter; + KIRQL oldIrql; + KeAcquireSpinLock(&g_adapterStateLock, &oldIrql); + adapter = g_adapter; + if (adapter && generation) + *generation = ReadCounter(&g_adapterGeneration); + KeReleaseSpinLock(&g_adapterStateLock, oldIrql); + + if (!adapter) + ExReleaseRundownProtection(&g_adapterRundown); + return adapter; +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +void BrokerDeviceReleaseAdapter() +{ + ExReleaseRundownProtection(&g_adapterRundown); +} diff --git a/idd/LGAudio/BrokerDevice.h b/idd/LGAudio/BrokerDevice.h new file mode 100644 index 00000000..f78aaaa3 --- /dev/null +++ b/idd/LGAudio/BrokerDevice.h @@ -0,0 +1,77 @@ +/** + * 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" + +class CAudioAdapter; + +struct LGAudioPortClsDeviceContext +{ + PVOID volatile adapter; +}; + +constexpr ULONG kLGAudioPortClsDeviceExtensionSize = + PORT_CLASS_DEVICE_EXTENSION_SIZE + + sizeof(LGAudioPortClsDeviceContext); + +inline LGAudioPortClsDeviceContext *GetLGAudioDeviceContext( + _In_ PDEVICE_OBJECT deviceObject) +{ + return reinterpret_cast( + static_cast(deviceObject->DeviceExtension) + + PORT_CLASS_DEVICE_EXTENSION_SIZE); +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS BrokerDeviceInitialize(_In_ PDRIVER_OBJECT driverObject); + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceShutdown(); + +_IRQL_requires_max_(DISPATCH_LEVEL) +bool BrokerDeviceIsControlDevice(_In_ PDEVICE_OBJECT deviceObject); + +_IRQL_requires_max_(APC_LEVEL) +NTSTATUS BrokerDeviceDispatch( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp); + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceNotifyPnpDeviceAdded(); + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceNotifyPnpDeviceRemoved(); + +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS BrokerDeviceAttachAdapter( + _In_ PDEVICE_OBJECT deviceObject, + _In_ CAudioAdapter *adapter); + +_IRQL_requires_max_(PASSIVE_LEVEL) +void BrokerDeviceDetachAdapter(_In_ PDEVICE_OBJECT deviceObject); + +_IRQL_requires_max_(DISPATCH_LEVEL) +CAudioAdapter *BrokerDeviceAcquireAdapter( + _Out_opt_ PULONGLONG generation); + +_IRQL_requires_max_(DISPATCH_LEVEL) +void BrokerDeviceReleaseAdapter(); diff --git a/idd/LGAudio/Driver.cpp b/idd/LGAudio/Driver.cpp index 09afb44a..5c2444e3 100644 --- a/idd/LGAudio/Driver.cpp +++ b/idd/LGAudio/Driver.cpp @@ -20,18 +20,77 @@ #include "Common.h" #include "Adapter.h" +#include "BrokerDevice.h" extern "C" DRIVER_INITIALIZE DriverEntry; namespace { +PDRIVER_UNLOAD g_portClsUnload = nullptr; + NTSTATUS AddAudioDevice( _In_ PDRIVER_OBJECT driverObject, _In_ PDEVICE_OBJECT physicalDeviceObject) { constexpr ULONG kSubdeviceCount = 4; - return PcAddAdapterDevice(driverObject, physicalDeviceObject, - StartAudioDevice, kSubdeviceCount, 0); + const NTSTATUS status = PcAddAdapterDevice(driverObject, + physicalDeviceObject, StartAudioDevice, kSubdeviceCount, + kLGAudioPortClsDeviceExtensionSize); + if (NT_SUCCESS(status)) + BrokerDeviceNotifyPnpDeviceAdded(); + return status; +} + +NTSTATUS DispatchIrp( + _In_ PDEVICE_OBJECT deviceObject, + _Inout_ PIRP irp) +{ + if (BrokerDeviceIsControlDevice(deviceObject)) + return BrokerDeviceDispatch(deviceObject, irp); + + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + const bool removeDevice = + stack->MajorFunction == IRP_MJ_PNP && + stack->MinorFunction == IRP_MN_REMOVE_DEVICE; + if (stack->MajorFunction == IRP_MJ_PNP && + (removeDevice || + stack->MinorFunction == IRP_MN_STOP_DEVICE || + stack->MinorFunction == IRP_MN_SURPRISE_REMOVAL)) + BrokerDeviceDetachAdapter(deviceObject); + + const NTSTATUS status = PcDispatchIrp(deviceObject, irp); + if (removeDevice) + BrokerDeviceNotifyPnpDeviceRemoved(); + return status; +} + +void DriverUnload(_In_ PDRIVER_OBJECT driverObject) +{ + BrokerDeviceShutdown(); + if (g_portClsUnload) + g_portClsUnload(driverObject); +} + +void InstallDispatchHooks(_In_ PDRIVER_OBJECT driverObject) +{ + constexpr UCHAR majors[] = + { + IRP_MJ_CLOSE, + IRP_MJ_CREATE, + IRP_MJ_DEVICE_CONTROL, + IRP_MJ_FLUSH_BUFFERS, + IRP_MJ_PNP, + IRP_MJ_POWER, + IRP_MJ_QUERY_SECURITY, + IRP_MJ_READ, + IRP_MJ_SET_SECURITY, + IRP_MJ_SYSTEM_CONTROL, + IRP_MJ_WRITE, + IRP_MJ_CLEANUP + }; + + for (const UCHAR major : majors) + driverObject->MajorFunction[major] = DispatchIrp; } } @@ -39,8 +98,22 @@ extern "C" NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) { - return PcInitializeAdapterDriver(driverObject, registryPath, + NTSTATUS status = PcInitializeAdapterDriver(driverObject, registryPath, AddAudioDevice); + if (!NT_SUCCESS(status)) + return status; + + g_portClsUnload = driverObject->DriverUnload; + InstallDispatchHooks(driverObject); + driverObject->DriverUnload = DriverUnload; + + const NTSTATUS brokerStatus = BrokerDeviceInitialize(driverObject); + if (!NT_SUCCESS(brokerStatus)) + { + LGAUDIO_TRACE_ERROR("broker initialization unavailable: 0x%08X", + brokerStatus); + } + return STATUS_SUCCESS; } #pragma warning(push) diff --git a/idd/LGAudio/LGAudio.vcxproj b/idd/LGAudio/LGAudio.vcxproj index 9e6cfb16..51387873 100644 --- a/idd/LGAudio/LGAudio.vcxproj +++ b/idd/LGAudio/LGAudio.vcxproj @@ -54,7 +54,7 @@ - $(ProjectDir);$(DDK_INC_PATH);%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\LGCommon;$(DDK_INC_PATH);%(AdditionalIncludeDirectories) _WIN32;_WIN32_WINNT=0x0A00;UNICODE;_UNICODE;PC_IMPLEMENTATION;_USE_WAVERT_;_NEW_DELETE_OPERATORS_;%(PreprocessorDefinitions) stdcpp17 Level4 @@ -66,7 +66,7 @@ _WIN32;_WIN32_WINNT=0x0A00;UNICODE;_UNICODE;PC_IMPLEMENTATION;_USE_WAVERT_;%(PreprocessorDefinitions) - portcls.lib;stdunk.lib;libcntpr.lib;%(AdditionalDependencies) + portcls.lib;stdunk.lib;libcntpr.lib;wdmsec.lib;%(AdditionalDependencies) SHA256 @@ -75,6 +75,7 @@ + @@ -86,6 +87,7 @@ + @@ -93,6 +95,7 @@ + diff --git a/idd/LGAudio/LGAudio.vcxproj.filters b/idd/LGAudio/LGAudio.vcxproj.filters index 04b66721..7be7ba55 100644 --- a/idd/LGAudio/LGAudio.vcxproj.filters +++ b/idd/LGAudio/LGAudio.vcxproj.filters @@ -18,6 +18,7 @@ Driver Driver + Driver Driver Driver Miniport @@ -29,6 +30,7 @@ Driver Driver + Driver Driver Driver Miniport @@ -36,6 +38,9 @@ Miniport Utilities Utilities + + Driver + Package diff --git a/idd/LGCommon/LGAudioBroker.h b/idd/LGCommon/LGAudioBroker.h new file mode 100644 index 00000000..9dff30fc --- /dev/null +++ b/idd/LGCommon/LGAudioBroker.h @@ -0,0 +1,181 @@ +/** + * 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 + +#if defined(_KERNEL_MODE) || defined(_NTDDK_) || defined(_WDMDDK_) +extern "C" +{ +#include +} +#else +#include +#include +#endif + +static constexpr wchar_t LGAUDIO_BROKER_NT_DEVICE_NAME[] = + L"\\Device\\LookingGlassAudioBroker"; +static constexpr wchar_t LGAUDIO_BROKER_DOS_DEVICE_NAME[] = + L"\\DosDevices\\LookingGlassAudioBroker"; +static constexpr wchar_t LGAUDIO_BROKER_USER_DEVICE_PATH[] = + L"\\\\.\\LookingGlassAudioBroker"; + +// This GUID identifies the security policy for the named control device. It +// is not a PnP device-interface class. +static const GUID LGAUDIO_BROKER_DEVICE_CLASS_GUID = + { 0x98f877f1, 0x73d6, 0x4429, + { 0xb4, 0x90, 0x28, 0xc5, 0x30, 0xd2, 0x38, 0xcd } }; + +static constexpr UINT32 LGAUDIO_BROKER_ABI_VERSION = 1U; + +enum LGAudioBrokerCapability : UINT32 +{ + LGAUDIO_BROKER_CAP_CONTROL = 1U << 0, + // These remain clear until the corresponding data plane is implemented. + LGAUDIO_BROKER_CAP_RENDER_READ = 1U << 1, + LGAUDIO_BROKER_CAP_CAPTURE_WRITE = 1U << 2, + LGAUDIO_BROKER_CAP_CLOCK_CONTROL = 1U << 3 +}; + +enum LGAudioBrokerStateFlag : UINT32 +{ + LGAUDIO_BROKER_STATE_ADAPTER_PRESENT = 1U << 0, + LGAUDIO_BROKER_STATE_BOUND = 1U << 1, + LGAUDIO_BROKER_STATE_DATA_PLANE_ACTIVE = 1U << 2 +}; + +enum LGAudioBrokerStreamStateValue : UINT32 +{ + LGAUDIO_BROKER_STREAM_UNAVAILABLE = 0, + LGAUDIO_BROKER_STREAM_STOPPED = 1, + LGAUDIO_BROKER_STREAM_ACQUIRED = 2, + LGAUDIO_BROKER_STREAM_PAUSED = 3, + LGAUDIO_BROKER_STREAM_RUNNING = 4 +}; + +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_QUERY_CAPS = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x900, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_QUERY_STATE = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x901, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_BIND = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x902, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_UNBIND = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x903, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); +static constexpr UINT32 IOCTL_LGAUDIO_BROKER_QUERY_STATS = + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x904, METHOD_BUFFERED, + FILE_READ_ACCESS | FILE_WRITE_ACCESS); + +#pragma pack(push, 8) + +struct LGAudioBrokerHeader +{ + UINT32 size; + UINT32 version; +}; + +struct LGAudioBrokerFormat +{ + UINT32 sampleRate; + UINT32 channelMask; + UINT16 channels; + UINT16 containerBits; + UINT16 validBits; + UINT16 blockAlign; +}; + +struct LGAudioBrokerStreamState +{ + UINT64 generation; + UINT32 state; + UINT32 flags; + LGAudioBrokerFormat format; +}; + +struct LGAudioBrokerCaps +{ + UINT32 size; + UINT32 version; + UINT32 minimumVersion; + UINT32 maximumVersion; + UINT32 capabilities; + UINT32 maximumRenderTransfer; + UINT32 maximumCaptureTransfer; + UINT32 reserved0; + UINT64 reserved[4]; +}; + +struct LGAudioBrokerState +{ + UINT32 size; + UINT32 version; + UINT32 flags; + UINT32 reserved0; + UINT64 adapterGeneration; + UINT64 eventSequence; + UINT64 bindingGeneration; + LGAudioBrokerStreamState render; + LGAudioBrokerStreamState capture; +}; + +struct LGAudioBrokerBind +{ + UINT32 size; + UINT32 version; + UINT32 flags; + UINT32 reserved0; + UINT64 adapterGeneration; + UINT64 bindingGeneration; + UINT64 reserved[2]; +}; + +struct LGAudioBrokerStats +{ + UINT32 size; + UINT32 version; + UINT64 adapterGeneration; + UINT64 eventSequence; + UINT64 bindingGeneration; + UINT64 bindCount; + UINT64 unbindCount; + UINT64 cleanupCount; + UINT64 rejectedRequestCount; + UINT64 reserved[2]; +}; + +#pragma pack(pop) + +static_assert(sizeof(LGAudioBrokerHeader) == 8, + "LGAudio broker header layout changed"); +static_assert(sizeof(LGAudioBrokerFormat) == 16, + "LGAudio broker format layout changed"); +static_assert(sizeof(LGAudioBrokerStreamState) == 32, + "LGAudio broker stream-state layout changed"); +static_assert(sizeof(LGAudioBrokerCaps) == 64, + "LGAudio broker capabilities layout changed"); +static_assert(sizeof(LGAudioBrokerState) == 104, + "LGAudio broker state layout changed"); +static_assert(sizeof(LGAudioBrokerBind) == 48, + "LGAudio broker binding layout changed"); +static_assert(sizeof(LGAudioBrokerStats) == 80, + "LGAudio broker statistics layout changed");