[PATCH v5 1/5] winepulse.drv: Expose audio devices to the application.
Andrew Eikum
aeikum at codeweavers.com
Mon Feb 21 14:23:00 CST 2022
Signed-off-by: Andrew Eikum <aeikum at codeweavers.com>
On Fri, Feb 18, 2022 at 08:37:59PM +0200, Gabriel Ivăncescu wrote:
> This exposes the actual devices (and virtual sinks/sources) as reported by
> PulseAudio to an application, allowing it to select the devices itself and,
> for example, record from (or render to) two devices at the same time. The
> "PulseAudio" device (which is movable) is still the default, as before,
> with the same GUID to preserve compatibility with existing setups.
>
> Based on a patch by Mark Harmstone <mark at harmstone.com>, with changes by
> Sebastian Lackner <sebastian at fds-team.de>.
>
> Signed-off-by: Gabriel Ivăncescu <gabrielopcode at gmail.com>
> ---
>
> Minor issue when allocating, it multiplied by sizeof(WCHAR) again...
>
> dlls/winepulse.drv/mmdevdrv.c | 205 ++++++++++++++++++++++++++++------
> dlls/winepulse.drv/pulse.c | 140 +++++++++++++++++++++--
> dlls/winepulse.drv/unixlib.h | 20 ++++
> 3 files changed, 321 insertions(+), 44 deletions(-)
>
> diff --git a/dlls/winepulse.drv/mmdevdrv.c b/dlls/winepulse.drv/mmdevdrv.c
> index e3d5ea9..8ef3575 100644
> --- a/dlls/winepulse.drv/mmdevdrv.c
> +++ b/dlls/winepulse.drv/mmdevdrv.c
> @@ -69,6 +69,8 @@ static GUID pulse_render_guid =
> static GUID pulse_capture_guid =
> { 0x25da76d0, 0x033c, 0x4235, { 0x90, 0x02, 0x19, 0xf4, 0x88, 0x94, 0xac, 0x6f } };
>
> +static const WCHAR *drv_key_devicesW = L"Software\\Wine\\Drivers\\winepulse.drv\\devices";
> +
> static CRITICAL_SECTION session_cs;
> static CRITICAL_SECTION_DEBUG session_cs_debug = {
> 0, 0, &session_cs,
> @@ -145,9 +147,9 @@ struct ACImpl {
>
> AudioSession *session;
> AudioSessionWrapper *session_wrapper;
> -};
>
> -static const WCHAR defaultW[] = L"PulseAudio";
> + char pulse_name[0];
> +};
>
> static const IAudioClient3Vtbl AudioClient3_Vtbl;
> static const IAudioRenderClientVtbl AudioRenderClient_Vtbl;
> @@ -267,39 +269,108 @@ static void set_stream_volumes(ACImpl *This)
> pulse_call(set_volumes, ¶ms);
> }
>
> -HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, const WCHAR ***ids, GUID **keys,
> - UINT *num, UINT *def_index)
> +static void get_device_guid(HKEY drv_key, EDataFlow flow, const char *pulse_name, GUID *guid)
> {
> - WCHAR *id;
> + WCHAR key_name[MAX_PULSE_NAME_LEN + 2];
> + DWORD type, size = sizeof(*guid);
> + LSTATUS status;
> + HKEY dev_key;
>
> - TRACE("%d %p %p %p\n", flow, ids, num, def_index);
> + if (!pulse_name[0]) {
> + *guid = (flow == eRender) ? pulse_render_guid : pulse_capture_guid;
> + return;
> + }
>
> - *num = 1;
> - *def_index = 0;
> + if (!drv_key) {
> + CoCreateGuid(guid);
> + return;
> + }
>
> - *ids = HeapAlloc(GetProcessHeap(), 0, sizeof(**ids));
> - *keys = NULL;
> - if (!*ids)
> - return E_OUTOFMEMORY;
> + key_name[0] = (flow == eRender) ? '0' : '1';
> + key_name[1] = ',';
> + MultiByteToWideChar(CP_UNIXCP, 0, pulse_name, -1, key_name + 2, ARRAY_SIZE(key_name) - 2);
>
> - (*ids)[0] = id = HeapAlloc(GetProcessHeap(), 0, sizeof(defaultW));
> - *keys = HeapAlloc(GetProcessHeap(), 0, sizeof(**keys));
> - if (!*keys || !id) {
> - HeapFree(GetProcessHeap(), 0, id);
> - HeapFree(GetProcessHeap(), 0, *keys);
> - HeapFree(GetProcessHeap(), 0, *ids);
> - *ids = NULL;
> - *keys = NULL;
> - return E_OUTOFMEMORY;
> + status = RegCreateKeyExW(drv_key, key_name, 0, NULL, 0, KEY_READ | KEY_WRITE | KEY_WOW64_64KEY,
> + NULL, &dev_key, NULL);
> + if (status != ERROR_SUCCESS) {
> + ERR("Failed to open registry key for device %s: %u\n", pulse_name, status);
> + CoCreateGuid(guid);
> + return;
> }
> - memcpy(id, defaultW, sizeof(defaultW));
>
> - if (flow == eRender)
> - (*keys)[0] = pulse_render_guid;
> - else
> - (*keys)[0] = pulse_capture_guid;
> + status = RegQueryValueExW(dev_key, L"guid", 0, &type, (BYTE*)guid, &size);
> + if (status != ERROR_SUCCESS || type != REG_BINARY || size != sizeof(*guid)) {
> + CoCreateGuid(guid);
> + status = RegSetValueExW(dev_key, L"guid", 0, REG_BINARY, (BYTE*)guid, sizeof(*guid));
> + if (status != ERROR_SUCCESS)
> + ERR("Failed to store device GUID for %s to registry: %u\n", pulse_name, status);
> + }
> + RegCloseKey(dev_key);
> +}
>
> - return S_OK;
> +HRESULT WINAPI AUDDRV_GetEndpointIDs(EDataFlow flow, WCHAR ***ids_out, GUID **keys,
> + UINT *num, UINT *def_index)
> +{
> + struct get_endpoint_ids_params params;
> + GUID *guids = NULL;
> + WCHAR **ids = NULL;
> + unsigned int i = 0;
> + LSTATUS status;
> + HKEY drv_key;
> +
> + TRACE("%d %p %p %p\n", flow, ids_out, num, def_index);
> +
> + params.flow = flow;
> + params.size = MAX_PULSE_NAME_LEN * 4;
> + params.endpoints = NULL;
> + do {
> + HeapFree(GetProcessHeap(), 0, params.endpoints);
> + params.endpoints = HeapAlloc(GetProcessHeap(), 0, params.size);
> + pulse_call(get_endpoint_ids, ¶ms);
> + } while(params.result == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER));
> +
> + if (FAILED(params.result))
> + goto end;
> +
> + ids = HeapAlloc(GetProcessHeap(), 0, params.num * sizeof(*ids));
> + guids = HeapAlloc(GetProcessHeap(), 0, params.num * sizeof(*guids));
> + if (!ids || !guids) {
> + params.result = E_OUTOFMEMORY;
> + goto end;
> + }
> +
> + status = RegCreateKeyExW(HKEY_CURRENT_USER, drv_key_devicesW, 0, NULL, 0,
> + KEY_WRITE | KEY_WOW64_64KEY, NULL, &drv_key, NULL);
> + if (status != ERROR_SUCCESS) {
> + ERR("Failed to open devices registry key: %u\n", status);
> + drv_key = NULL;
> + }
> +
> + for (i = 0; i < params.num; i++) {
> + unsigned int size = (wcslen(params.endpoints[i].name) + 1) * sizeof(WCHAR);
> + if (!(ids[i] = HeapAlloc(GetProcessHeap(), 0, size))) {
> + params.result = E_OUTOFMEMORY;
> + break;
> + }
> + memcpy(ids[i], params.endpoints[i].name, size);
> + get_device_guid(drv_key, flow, params.endpoints[i].pulse_name, &guids[i]);
> + }
> + if (drv_key)
> + RegCloseKey(drv_key);
> +
> +end:
> + HeapFree(GetProcessHeap(), 0, params.endpoints);
> + if (FAILED(params.result)) {
> + HeapFree(GetProcessHeap(), 0, guids);
> + while (i--) HeapFree(GetProcessHeap(), 0, ids[i]);
> + HeapFree(GetProcessHeap(), 0, ids);
> + } else {
> + *ids_out = ids;
> + *keys = guids;
> + *num = params.num;
> + *def_index = params.default_idx;
> + }
> + return params.result;
> }
>
> int WINAPI AUDDRV_GetPriority(void)
> @@ -314,23 +385,87 @@ int WINAPI AUDDRV_GetPriority(void)
> return SUCCEEDED(params.result) ? Priority_Preferred : Priority_Unavailable;
> }
>
> +static BOOL get_pulse_name_by_guid(const GUID *guid, char pulse_name[MAX_PULSE_NAME_LEN], EDataFlow *flow)
> +{
> + WCHAR key_name[MAX_PULSE_NAME_LEN + 2];
> + DWORD key_name_size;
> + DWORD index = 0;
> + HKEY key;
> +
> + /* Return empty string for default PulseAudio device */
> + pulse_name[0] = 0;
> + if (IsEqualGUID(guid, &pulse_render_guid)) {
> + *flow = eRender;
> + return TRUE;
> + } else if (IsEqualGUID(guid, &pulse_capture_guid)) {
> + *flow = eCapture;
> + return TRUE;
> + }
> +
> + if (RegOpenKeyExW(HKEY_CURRENT_USER, drv_key_devicesW, 0, KEY_READ | KEY_WOW64_64KEY, &key) != ERROR_SUCCESS) {
> + WARN("No devices found in registry\n");
> + return FALSE;
> + }
> +
> + for (;;) {
> + DWORD size, type;
> + LSTATUS status;
> + GUID reg_guid;
> + HKEY dev_key;
> +
> + key_name_size = ARRAY_SIZE(key_name);
> + if (RegEnumKeyExW(key, index++, key_name, &key_name_size, NULL, NULL, NULL, NULL) != ERROR_SUCCESS)
> + break;
> +
> + if (RegOpenKeyExW(key, key_name, 0, KEY_READ | KEY_WOW64_64KEY, &dev_key) != ERROR_SUCCESS) {
> + ERR("Couldn't open key: %s\n", wine_dbgstr_w(key_name));
> + continue;
> + }
> +
> + size = sizeof(reg_guid);
> + status = RegQueryValueExW(dev_key, L"guid", 0, &type, (BYTE *)®_guid, &size);
> + RegCloseKey(dev_key);
> +
> + if (status == ERROR_SUCCESS && type == REG_BINARY && size == sizeof(reg_guid) && IsEqualGUID(®_guid, guid)) {
> + RegCloseKey(key);
> +
> + TRACE("Found matching device key: %s\n", wine_dbgstr_w(key_name));
> +
> + if (key_name[0] == '0')
> + *flow = eRender;
> + else if (key_name[0] == '1')
> + *flow = eCapture;
> + else {
> + WARN("Unknown device type: %c\n", key_name[0]);
> + return FALSE;
> + }
> +
> + return WideCharToMultiByte(CP_UNIXCP, 0, key_name + 2, -1, pulse_name, MAX_PULSE_NAME_LEN, NULL, NULL);
> + }
> + }
> +
> + RegCloseKey(key);
> + WARN("No matching device in registry for GUID %s\n", debugstr_guid(guid));
> + return FALSE;
> +}
> +
> HRESULT WINAPI AUDDRV_GetAudioEndpoint(GUID *guid, IMMDevice *dev, IAudioClient **out)
> {
> ACImpl *This;
> + char pulse_name[MAX_PULSE_NAME_LEN];
> EDataFlow dataflow;
> + unsigned len;
> HRESULT hr;
>
> TRACE("%s %p %p\n", debugstr_guid(guid), dev, out);
> - if (IsEqualGUID(guid, &pulse_render_guid))
> - dataflow = eRender;
> - else if (IsEqualGUID(guid, &pulse_capture_guid))
> - dataflow = eCapture;
> - else
> - return E_UNEXPECTED;
> +
> + if (!get_pulse_name_by_guid(guid, pulse_name, &dataflow))
> + return AUDCLNT_E_DEVICE_INVALIDATED;
>
> *out = NULL;
>
> - This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*This));
> + len = strlen(pulse_name) + 1;
> + This = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, FIELD_OFFSET(ACImpl, pulse_name[len]));
> if (!This)
> return E_OUTOFMEMORY;
>
> @@ -342,6 +477,7 @@ HRESULT WINAPI AUDDRV_GetAudioEndpoint(GUID *guid, IMMDevice *dev, IAudioClient
> This->IAudioStreamVolume_iface.lpVtbl = &AudioStreamVolume_Vtbl;
> This->dataflow = dataflow;
> This->parent = dev;
> + memcpy(This->pulse_name, pulse_name, len);
>
> hr = CoCreateFreeThreadedMarshaler((IUnknown*)&This->IAudioClient3_iface, &This->marshal);
> if (FAILED(hr)) {
> @@ -609,6 +745,7 @@ static HRESULT WINAPI AudioClient_Initialize(IAudioClient3 *iface,
> }
>
> params.name = name = get_application_name();
> + params.pulse_name = This->pulse_name;
> params.dataflow = This->dataflow;
> params.mode = mode;
> params.flags = flags;
> diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c
> index 3e65936..311c9bb 100644
> --- a/dlls/winepulse.drv/pulse.c
> +++ b/dlls/winepulse.drv/pulse.c
> @@ -81,6 +81,12 @@ typedef struct _ACPacket
> UINT32 discont;
> } ACPacket;
>
> +typedef struct _PhysDevice {
> + struct list entry;
> + WCHAR *name;
> + char pulse_name[0];
> +} PhysDevice;
> +
> static pa_context *pulse_ctx;
> static pa_mainloop *pulse_ml;
>
> @@ -89,6 +95,8 @@ static WAVEFORMATEXTENSIBLE pulse_fmt[2];
> static REFERENCE_TIME pulse_min_period[2], pulse_def_period[2];
>
> static UINT g_phys_speakers_mask = 0;
> +static struct list g_phys_speakers = LIST_INIT(g_phys_speakers);
> +static struct list g_phys_sources = LIST_INIT(g_phys_sources);
>
> static const REFERENCE_TIME MinimumPeriod = 30000;
> static const REFERENCE_TIME DefaultPeriod = 100000;
> @@ -128,6 +136,20 @@ static void dump_attr(const pa_buffer_attr *attr)
> TRACE("prebuf: %u\n", attr->prebuf);
> }
>
> +static void free_phys_device_lists(void)
> +{
> + static struct list *const lists[] = { &g_phys_speakers, &g_phys_sources, NULL };
> + struct list *const *list = lists;
> + PhysDevice *dev, *dev_next;
> +
> + do {
> + LIST_FOR_EACH_ENTRY_SAFE(dev, dev_next, *list, PhysDevice, entry) {
> + free(dev->name);
> + free(dev);
> + }
> + } while (*(++list));
> +}
> +
> /* copied from kernelbase */
> static int muldiv(int a, int b, int c)
> {
> @@ -190,6 +212,7 @@ static NTSTATUS pulse_process_attach(void *args)
>
> static NTSTATUS pulse_process_detach(void *args)
> {
> + free_phys_device_lists();
> if (pulse_ctx)
> {
> pa_context_disconnect(pulse_ctx);
> @@ -215,6 +238,44 @@ static NTSTATUS pulse_main_loop(void *args)
> return STATUS_SUCCESS;
> }
>
> +static NTSTATUS pulse_get_endpoint_ids(void *args)
> +{
> + struct get_endpoint_ids_params *params = args;
> + struct list *list = (params->flow == eRender) ? &g_phys_speakers : &g_phys_sources;
> + struct endpoint *endpoint = params->endpoints;
> + DWORD len, name_len, needed;
> + PhysDevice *dev;
> + char *ptr;
> +
> + params->num = list_count(list);
> + needed = params->num * sizeof(*params->endpoints);
> + ptr = (char*)(endpoint + params->num);
> +
> + LIST_FOR_EACH_ENTRY(dev, list, PhysDevice, entry) {
> + name_len = lstrlenW(dev->name) + 1;
> + len = strlen(dev->pulse_name) + 1;
> + needed += name_len * sizeof(WCHAR) + ((len + 1) & ~1);
> +
> + if (needed <= params->size) {
> + endpoint->name = (WCHAR*)ptr;
> + memcpy(endpoint->name, dev->name, name_len * sizeof(WCHAR));
> + ptr += name_len * sizeof(WCHAR);
> + endpoint->pulse_name = ptr;
> + memcpy(endpoint->pulse_name, dev->pulse_name, len);
> + ptr += (len + 1) & ~1;
> + endpoint++;
> + }
> + }
> + params->default_idx = 0;
> +
> + if (needed > params->size) {
> + params->size = needed;
> + params->result = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
> + } else
> + params->result = S_OK;
> + return STATUS_SUCCESS;
> +}
> +
> static void pulse_contextcallback(pa_context *c, void *userdata)
> {
> switch (pa_context_get_state(c)) {
> @@ -357,12 +418,49 @@ static DWORD pulse_channel_map_to_channel_mask(const pa_channel_map *map)
> return mask;
> }
>
> -/* For default PulseAudio render device, OR together all of the
> - * PKEY_AudioEndpoint_PhysicalSpeakers values of the sinks. */
> +static void pulse_add_device(struct list *list, const char *pulse_name, const char *name)
> +{
> + DWORD len = strlen(pulse_name), name_len = strlen(name);
> + PhysDevice *dev = malloc(FIELD_OFFSET(PhysDevice, pulse_name[len + 1]));
> + WCHAR *wname;
> +
> + if (!dev)
> + return;
> +
> + if (!(wname = malloc((name_len + 1) * sizeof(WCHAR)))) {
> + free(dev);
> + return;
> + }
> +
> + if (!(name_len = ntdll_umbstowcs(name, name_len, wname, name_len)) ||
> + !(dev->name = realloc(wname, (name_len + 1) * sizeof(WCHAR)))) {
> + free(wname);
> + free(dev);
> + return;
> + }
> + dev->name[name_len] = 0;
> + memcpy(dev->pulse_name, pulse_name, len + 1);
> +
> + list_add_tail(list, &dev->entry);
> +}
> +
> static void pulse_phys_speakers_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
> {
> - if (i)
> - g_phys_speakers_mask |= pulse_channel_map_to_channel_mask(&i->channel_map);
> + if (!i || !i->name || !i->name[0])
> + return;
> +
> + /* For default PulseAudio render device, OR together all of the
> + * PKEY_AudioEndpoint_PhysicalSpeakers values of the sinks. */
> + g_phys_speakers_mask |= pulse_channel_map_to_channel_mask(&i->channel_map);
> +
> + pulse_add_device(&g_phys_speakers, i->name, i->description);
> +}
> +
> +static void pulse_phys_sources_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata)
> +{
> + if (!i || !i->name || !i->name[0])
> + return;
> + pulse_add_device(&g_phys_sources, i->name, i->description);
> }
>
> /* For most hardware on Windows, users must choose a configuration with an even
> @@ -579,7 +677,14 @@ static NTSTATUS pulse_test_connect(void *args)
> pulse_probe_settings(1, &pulse_fmt[0]);
> pulse_probe_settings(0, &pulse_fmt[1]);
>
> + free_phys_device_lists();
> + list_init(&g_phys_speakers);
> + list_init(&g_phys_sources);
> g_phys_speakers_mask = 0;
> +
> + pulse_add_device(&g_phys_speakers, "", "PulseAudio");
> + pulse_add_device(&g_phys_sources, "", "PulseAudio");
> +
> o = pa_context_get_sink_info_list(pulse_ctx, &pulse_phys_speakers_cb, NULL);
> if (o) {
> while (pa_mainloop_iterate(pulse_ml, 1, &ret) >= 0 &&
> @@ -588,6 +693,14 @@ static NTSTATUS pulse_test_connect(void *args)
> pa_operation_unref(o);
> }
>
> + o = pa_context_get_source_info_list(pulse_ctx, &pulse_phys_sources_cb, NULL);
> + if (o) {
> + while (pa_mainloop_iterate(pulse_ml, 1, &ret) >= 0 &&
> + pa_operation_get_state(o) == PA_OPERATION_RUNNING)
> + {}
> + pa_operation_unref(o);
> + }
> +
> pa_context_unref(pulse_ctx);
> pulse_ctx = NULL;
> pa_mainloop_free(pulse_ml);
> @@ -771,8 +884,9 @@ static HRESULT pulse_spec_from_waveformat(struct pulse_stream *stream, const WAV
> return S_OK;
> }
>
> -static HRESULT pulse_stream_connect(struct pulse_stream *stream, UINT32 period_bytes)
> +static HRESULT pulse_stream_connect(struct pulse_stream *stream, const char *pulse_name, UINT32 period_bytes)
> {
> + pa_stream_flags_t flags = PA_STREAM_START_CORKED | PA_STREAM_START_UNMUTED | PA_STREAM_ADJUST_LATENCY;
> int ret;
> char buffer[64];
> static LONG number;
> @@ -797,12 +911,17 @@ static HRESULT pulse_stream_connect(struct pulse_stream *stream, UINT32 period_b
> attr.maxlength = stream->bufsize_frames * pa_frame_size(&stream->ss);
> attr.prebuf = pa_frame_size(&stream->ss);
> dump_attr(&attr);
> +
> + /* If specific device was requested, use it exactly */
> + if (pulse_name[0])
> + flags |= PA_STREAM_DONT_MOVE;
> + else
> + pulse_name = NULL; /* use default */
> +
> if (stream->dataflow == eRender)
> - ret = pa_stream_connect_playback(stream->stream, NULL, &attr,
> - PA_STREAM_START_CORKED|PA_STREAM_START_UNMUTED|PA_STREAM_ADJUST_LATENCY, NULL, NULL);
> + ret = pa_stream_connect_playback(stream->stream, pulse_name, &attr, flags, NULL, NULL);
> else
> - ret = pa_stream_connect_record(stream->stream, NULL, &attr,
> - PA_STREAM_START_CORKED|PA_STREAM_START_UNMUTED|PA_STREAM_ADJUST_LATENCY);
> + ret = pa_stream_connect_record(stream->stream, pulse_name, &attr, flags);
> if (ret < 0) {
> WARN("Returns %i\n", ret);
> return AUDCLNT_E_ENDPOINT_CREATE_FAILED;
> @@ -864,7 +983,7 @@ static NTSTATUS pulse_create_stream(void *args)
>
> stream->share = params->mode;
> stream->flags = params->flags;
> - hr = pulse_stream_connect(stream, stream->period_bytes);
> + hr = pulse_stream_connect(stream, params->pulse_name, stream->period_bytes);
> if (SUCCEEDED(hr)) {
> UINT32 unalign;
> const pa_buffer_attr *attr = pa_stream_get_buffer_attr(stream->stream);
> @@ -1967,6 +2086,7 @@ const unixlib_entry_t __wine_unix_call_funcs[] =
> pulse_process_attach,
> pulse_process_detach,
> pulse_main_loop,
> + pulse_get_endpoint_ids,
> pulse_create_stream,
> pulse_release_stream,
> pulse_start,
> diff --git a/dlls/winepulse.drv/unixlib.h b/dlls/winepulse.drv/unixlib.h
> index d28a73c..982704a 100644
> --- a/dlls/winepulse.drv/unixlib.h
> +++ b/dlls/winepulse.drv/unixlib.h
> @@ -19,6 +19,8 @@
> #include "wine/list.h"
> #include "wine/unixlib.h"
>
> +#define MAX_PULSE_NAME_LEN 256
> +
> struct pulse_stream;
>
> struct pulse_config
> @@ -32,14 +34,31 @@ struct pulse_config
> unsigned int speakers_mask;
> };
>
> +struct endpoint
> +{
> + WCHAR *name;
> + char *pulse_name;
> +};
> +
> struct main_loop_params
> {
> HANDLE event;
> };
>
> +struct get_endpoint_ids_params
> +{
> + EDataFlow flow;
> + struct endpoint *endpoints;
> + unsigned int size;
> + HRESULT result;
> + unsigned int num;
> + unsigned int default_idx;
> +};
> +
> struct create_stream_params
> {
> const char *name;
> + const char *pulse_name;
> EDataFlow dataflow;
> AUDCLNT_SHAREMODE mode;
> DWORD flags;
> @@ -191,6 +210,7 @@ enum unix_funcs
> process_attach,
> process_detach,
> main_loop,
> + get_endpoint_ids,
> create_stream,
> release_stream,
> start,
> --
> 2.34.1
>
>
More information about the wine-devel
mailing list