Merge GE-Proton11-12 (f85547f0) into rebase/11-12

GE-Proton11-12 now carries the three GE master commits 11-11.1 took early,
so our copies go:
- patches/protonprep-valve-staging.sh: GE's version (our two added
  apply lines for 98efe1cd and e15b1e15 are GE's own now).
- ntdll-take-in-flight-alert-when-wait-on-address-times-out.patch: GE's
  newer version (960acd0a, with its test).
- winewayland-advertise-openvr-openxr-device-extensions.patch and
  lsteamclient 0017: identical to GE's, merged without a change.

README.md: ours (updated for 11-12 in a later commit).
wine: still banner-proton/11.11 here; moved to banner-proton/11.12 in a
later commit.
This commit is contained in:
The412Banner committed 2026-11-03 15:25:52 +00:00
commit ef7f031388
102 files changed
+8094 -2363

No files matched your search

+18 -3
View File
@@ -33,6 +33,12 @@ THE GE-PROTON ISSUE TRACKER IS FOR ISSUES THAT ONLY OCCUR ON GE-PROTON BUT WORK
- Last known working GE-Proton version, if any:
- First known broken GE-Proton version, if any:
## Wine-Wayland / launcher
- Using Wine-Wayland (`PROTON_ENABLE_WAYLAND=1`): <!-- yes/no -->
- Same problem with Wine-Wayland disabled (XWayland): <!-- yes/no/not tested -->
- Launcher: <!-- Steam / umu / Heroic / Lutris / other -->
- Displays: <!-- count, resolution, refresh rate, scaling, VRR/HDR on? -->
## Launch options
```text
<!-- Paste the exact Steam launch options used, e.g. PROTON_LOG=1 %command% -->
@@ -49,10 +55,19 @@ For issues with the GE-Proton flatpak, report here:
https://github.com/flathub/com.valvesoftware.Steam.CompatibilityTool.Proton-GE
<!-- Please add `PROTON_LOG=1 %command%` to the game's launch options and drag
and drop the generated `$HOME/steam-$APPID.log` into this issue report. -->
and drop the generated `$HOME/steam-$APPID.log` into this issue report.
<!-- For media/audio/video regressions, please consider adding a focused WINEDEBUG log if requested by the maintainer, for example:
PROTON_LOG=1 WINEDEBUG="+seh,+dmo,+mfplat,+quartz,+strmbase,+mfreadwrite,+xaudio2,+mmdevapi,+dsound,+winmm,+pulse,+err" %command%
For a more detailed log, set PROTON_LOG to a list of extra channels instead of 1.
This keeps Proton's default logging and adds those channels. Don't set WINEDEBUG
directly, that replaces the defaults. Only use the line for your problem area,
these logs get large and can slow the game down:
- Wine-Wayland, cursor, windowing: PROTON_LOG=+waylanddrv,+cursor,+win
- Keyboard / IME: PROTON_LOG=+keyboard,+key,+imm
- Controllers: PROTON_LOG=+hid,+dinput,+xinput,+rawinput
- Audio: PROTON_LOG=+mmdevapi,+pulse,+xaudio2,+dsound
- Video / cutscenes: PROTON_LOG=+dmo,+mfplat,+quartz,+strmbase,+mfreadwrite,+wmvcore
- X11 (not Wine-Wayland): PROTON_LOG=+x11drv,+xrandr,+cursor
-->
## Symptoms <!-- What's the problem? -->
+21 -15
View File
@@ -13,11 +13,6 @@ on:
jobs:
upload:
name: Upload ${{ inputs.name }}
strategy:
matrix:
include:
- file: ${{ inputs.name }}.tar.gz
- file: ${{ inputs.name }}.sha512sum
runs-on: ubuntu-latest
@@ -29,19 +24,30 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download ${{ matrix.file }} artifact
- name: Download ${{ inputs.name }}.tar.gz artifact
uses: actions/download-artifact@v6
with:
name: ${{ matrix.file }}
name: ${{ inputs.name }}.tar.gz
- name: Upload ${{ matrix.file }} to release
- name: Download ${{ inputs.name }}.sha512sum artifact
uses: actions/download-artifact@v6
with:
name: ${{ inputs.name }}.sha512sum
# Never replace a build that is already on the release, e.g. one uploaded
# by hand. The tarball and its checksum go together, checked once in one
# job: if either is there, upload neither, so the checksum always matches
# the tarball.
- name: Upload ${{ inputs.name }} to release
shell: bash
run: >-
gh
--repo "${{ github.server_url }}/${{ github.repository }}"
release upload
${{ inputs.version }}
${{ matrix.file }}
--clobber
run: |
repo="${{ github.server_url }}/${{ github.repository }}"
if gh release view "${{ inputs.version }}" --repo "$repo" --json assets --jq '.assets[].name' \
| grep -Fxq -e "${{ inputs.name }}.tar.gz" -e "${{ inputs.name }}.sha512sum"; then
echo "::notice::A ${{ inputs.name }} build is already on release ${{ inputs.version }}, not uploading."
else
gh release upload "${{ inputs.version }}" \
"${{ inputs.name }}.tar.gz" "${{ inputs.name }}.sha512sum" --repo "$repo"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -3
View File
@@ -31,7 +31,7 @@ jobs:
shell: bash
run: |
git fetch origin +refs/tags/*:refs/tags/*
- name: Create Release
- name: Create Release
id: create
shell: bash
run: |
@@ -42,8 +42,7 @@ jobs:
--latest \
--title "${{ github.ref_name }} Released" \
--notes-from-tag \
--verify-tag \
--repo "${{ github.server_url }}/${{ github.repository }}"
--verify-tag
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
Submodule FEX updated: 741e146fdf...02e24ca5c0.
+14 -4
View File
@@ -776,13 +776,19 @@ DXVK_x86_64_MESON_ARGS = --bindir=$(DXVK_x86_64_DST)/lib/wine/dxvk/x86_64-window
DXVK_arm64ec_MESON_ARGS = --bindir=$(DXVK_arm64ec_DST)/lib/wine/dxvk/aarch64-windows
DXVK_HOST_DEPENDS = glslang
# Tag the version string of a component built from a modified tree (GE
# patches, or anything else uncommitted), so its developers can tell from a
# bug report that the build is not their upstream code.
GE_DIRTY_TAG := -GE-Proton(dirty)
ge-dirty-tag = $(if $(shell git -C $(1) status --porcelain 2>/dev/null),$(GE_DIRTY_TAG))
$(eval $(call rules-source,dxvk,$(SRCDIR)/dxvk))
$(eval $(call rules-meson,dxvk,i386,windows))
$(eval $(call rules-meson,dxvk,x86_64,windows))
$(eval $(call rules-meson,dxvk,arm64ec,windows))
$(OBJ)/.dxvk-post-source:
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/dxvk describe --always --abbrev=15 --dirty=0 || echo deadbeef)#' \
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/dxvk describe --always --abbrev=15 || echo deadbeef)$(call ge-dirty-tag,$(SRCDIR)/dxvk)#' \
$(SRCDIR)/dxvk/version.h.in > $(DXVK_SRC)/version.h.in
mkdir -p $(DST_LIBDIR)/wine/dxvk
rm -rf $(DST_LIBDIR)/wine/dxvk/version
@@ -810,7 +816,7 @@ $(eval $(call rules-source,d7vk,$(SRCDIR)/d7vk))
$(eval $(call rules-meson,d7vk,i386,windows))
$(OBJ)/.d7vk-post-source:
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/d7vk describe --always --abbrev=15 --dirty=0 || echo deadbeef)#' \
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/d7vk describe --always --abbrev=15 || echo deadbeef)$(call ge-dirty-tag,$(SRCDIR)/d7vk)#' \
$(SRCDIR)/d7vk/version.h.in > $(D7VK_SRC)/version.h.in
mkdir -p $(DST_LIBDIR)/wine/d7vk
rm -rf $(DST_LIBDIR)/wine/d7vk/version
@@ -957,7 +963,7 @@ $(eval $(call rules-meson,vkd3d-proton,arm64ec,windows))
$(OBJ)/.vkd3d-proton-post-source:
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/vkd3d-proton describe --always --exclude=* --abbrev=15 --dirty=0 || echo deadbeef)#' \
$(SRCDIR)/vkd3d-proton/vkd3d_build.h.in > $(VKD3D_PROTON_SRC)/vkd3d_build.h.in
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/vkd3d-proton describe --always --tags --dirty=+ || echo deadbeef)#' \
sed -re 's#@VCS_TAG@#$(shell git -C $(SRCDIR)/vkd3d-proton describe --always --tags || echo deadbeef)$(if $(call ge-dirty-tag,$(SRCDIR)/vkd3d-proton),$(GE_DIRTY_TAG)+)#' \
$(SRCDIR)/vkd3d-proton/vkd3d_version.h.in > $(VKD3D_PROTON_SRC)/vkd3d_version.h.in
mkdir -p $(DST_LIBDIR)/wine/vkd3d-proton
rm -rf $(DST_LIBDIR)/wine/vkd3d-proton/version
@@ -1506,6 +1512,10 @@ $(DIST_WINEMONO): $(DIST_WINEMONO_TARGET) $(WINEMONO_FNA_STAMP)
all-dist: $(DIST_WINEMONO)
# wineboot only installs the Wine Mono support package when it can find the
# runtime, through the wine-mono symlink made above.
default_pfx: $(DIST_WINEMONO)
mono-distclean::
rm -rf $(OBJ)/.wine-mono-dist
@@ -1516,7 +1526,7 @@ distclean: mono-distclean
## xalia
##
XALIA_VER := 0.5.0
XALIA_VER := 0.5.0.1
ifeq ($(TARGET_ARCH),arm64)
XALIA_ZIP := xalia-$(XALIA_VER)-net48-arm64-mono.zip
else
+1 -1
View File
@@ -1 +1 @@
GE-Proton11-11
GE-Proton11-12
+1 -1
Submodule d7vk updated: bcd66d0c95...06656b9e94.
+1 -1
Submodule dxvk updated: 2cb40186c4...f863776806.
@@ -461,7 +461,7 @@ NTSTATUS ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails( void *args )
{
struct ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails_params *params = (struct ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails_params *)args;
struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION008 *iface = (struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION008 *)params->u_iface;
char *u_pszFileName = steamclient_dos_to_unix_path( params->pszFileName, 0 );
char *u_pszFileName = steamclient_dos_to_unix_path_fixup_slash( params->pszFileName, 0 );
params->_ret = iface->GetFileDetails( u_pszFileName );
steamclient_free_path( u_pszFileName );
return 0;
@@ -472,7 +472,7 @@ NTSTATUS wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails( void *a
{
struct wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails_params *params = (struct wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION008_GetFileDetails_params *)args;
struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION008 *iface = (struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION008 *)params->u_iface;
char *u_pszFileName = steamclient_dos_to_unix_path( params->pszFileName, 0 );
char *u_pszFileName = steamclient_dos_to_unix_path_fixup_slash( params->pszFileName, 0 );
params->_ret = iface->GetFileDetails( u_pszFileName );
steamclient_free_path( u_pszFileName );
return 0;
@@ -461,7 +461,7 @@ NTSTATUS ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails( void *args )
{
struct ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails_params *params = (struct ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails_params *)args;
struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION009 *iface = (struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION009 *)params->u_iface;
char *u_pszFileName = steamclient_dos_to_unix_path( params->pszFileName, 0 );
char *u_pszFileName = steamclient_dos_to_unix_path_fixup_slash( params->pszFileName, 0 );
params->_ret = iface->GetFileDetails( u_pszFileName );
steamclient_free_path( u_pszFileName );
return 0;
@@ -472,7 +472,7 @@ NTSTATUS wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails( void *a
{
struct wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails_params *params = (struct wow64_ISteamApps_STEAMAPPS_INTERFACE_VERSION009_GetFileDetails_params *)args;
struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION009 *iface = (struct u_ISteamApps_STEAMAPPS_INTERFACE_VERSION009 *)params->u_iface;
char *u_pszFileName = steamclient_dos_to_unix_path( params->pszFileName, 0 );
char *u_pszFileName = steamclient_dos_to_unix_path_fixup_slash( params->pszFileName, 0 );
params->_ret = iface->GetFileDetails( u_pszFileName );
steamclient_free_path( u_pszFileName );
return 0;
+3 -1
View File
@@ -382,7 +382,7 @@ PATH_CONV_METHODS_UTOW = {
PATH_CONV_METHODS_WTOU = {
"ISteamApps_GetFileDetails": {
"pszFileName": {"array": False, "url": False},
"pszFileName": {"array": False, "url": False, "slash_in_installdir" : True},
},
### ISteamGameServer::SetModDir - "Just the folder name, not the whole path. I.e. "Spacewar"."
"ISteamHTMLSurface_LoadURL": {
@@ -1078,6 +1078,8 @@ def handle_method_cpp(method, classname, out, wow64):
for name, conv in filter(lambda x: x[0] in names, path_conv_wtou.items()):
if conv['array']:
out(f' const char **u_{name} = {prefix}steamclient_dos_to_unix_path_array( params->{name} );\n')
elif 'slash_in_installdir' in conv and conv['slash_in_installdir']:
out(f' char *u_{name} = steamclient_dos_to_unix_path_fixup_slash( params->{name}, {int(conv["url"])} );\n')
else:
out(f' char *u_{name} = steamclient_dos_to_unix_path( params->{name}, {int(conv["url"])} );\n')
@@ -87,6 +87,7 @@ void ge_overlay_bridge_enable_opengl_presenter(int32_t x, int32_t y,
void ge_overlay_bridge_present_opengl(int32_t x, int32_t y,
uint32_t width, uint32_t height);
void ge_overlay_bridge_focus(int focused);
void ge_overlay_bridge_pump(void);
int ge_overlay_bridge_filter_key(uint32_t time, uint32_t key, int pressed,
uint32_t utf32);
int ge_overlay_bridge_filter_pointer_button(uint32_t time, uint32_t button,
@@ -984,6 +984,31 @@ static Bool match_overlay_event(Display *display, XEvent *event, XPointer arg)
event->xany.serial == match->serial && event->type == match->type;
}
static Bool match_no_overlay_event(Display *display, XEvent *event,
XPointer arg)
{
(void)display;
(void)event;
(void)arg;
return False;
}
/* An X11 game polls Xlib continuously, so Steam's interposed XCheckIfEvent
* runs hundreds of times per second and services the overlay from there. The
* Wayland path only calls it when forwarding input, so poll it once per
* presented frame while the overlay is open. The predicate never matches. */
void ge_overlay_bridge_pump(void)
{
XEvent returned;
pthread_mutex_lock(&overlay_mutex);
if (overlay_active && overlay_initialized > 0 && overlay_display &&
overlay_check_if_event)
overlay_check_if_event(overlay_display, &returned,
match_no_overlay_event, NULL);
pthread_mutex_unlock(&overlay_mutex);
}
static uint32_t cursor_shape_from_glyph(unsigned int glyph)
{
switch (glyph)
+61 -15
View File
@@ -50,6 +50,10 @@ struct ge_overlay_wayland_surface
uint32_t fractional_scale_manager_name;
uint32_t pointer_serial;
int relative_motion_seen;
int relative_pending;
int last_motion_valid;
wl_fixed_t last_motion_x;
wl_fixed_t last_motion_y;
int bridge_registered;
int keyboard_focus;
int pointer_focus;
@@ -63,6 +67,7 @@ struct ge_overlay_wayland_surface
unsigned int refs;
uint8_t keys[256];
struct ge_steam_overlay_pointer_frame frame;
struct ge_steam_overlay_pointer_frame relative_frame;
};
static pthread_mutex_t input_surface_mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -315,10 +320,18 @@ void ge_overlay_wayland_set_cursor_position(int32_t x, int32_t y)
focused_pointer->pointer_x = x;
focused_pointer->pointer_y = y;
if (focused_pointer->overlay_cursor_subsurface)
{
wl_subsurface_set_position(
focused_pointer->overlay_cursor_subsurface,
x - focused_pointer->cursor_hotspot_x,
y - focused_pointer->cursor_hotspot_y);
/* The protocol applies the position on the parent's next commit,
* but Smithay compositors (niri, COSMIC) latch it with the
* subsurface's own commit. Committing the unchanged cursor
* surface works for both. */
wl_surface_commit(focused_pointer->overlay_cursor_surface);
wl_display_flush(focused_pointer->display);
}
}
pthread_mutex_unlock(&focused_pointer_mutex);
}
@@ -582,6 +595,11 @@ static void pointer_enter(void *data, struct wl_pointer *pointer,
surface->pointer_x = wl_fixed_to_int(x);
surface->pointer_y = wl_fixed_to_int(y);
surface->relative_motion_seen = 0;
surface->last_motion_x = x;
surface->last_motion_y = y;
surface->last_motion_valid = 1;
surface->relative_pending = 0;
memset(&surface->relative_frame, 0, sizeof(surface->relative_frame));
surface->frame.time = 0;
surface->frame.x = surface->pointer_x;
surface->frame.y = surface->pointer_y;
@@ -618,6 +636,9 @@ static void pointer_leave(void *data, struct wl_pointer *pointer,
surface->pointer_focus = 0;
surface->pointer_serial = 0;
surface->relative_motion_seen = 0;
surface->last_motion_valid = 0;
surface->relative_pending = 0;
memset(&surface->relative_frame, 0, sizeof(surface->relative_frame));
memset(&surface->frame, 0, sizeof(surface->frame));
update_bridge_focus(surface);
overlay_trace("Wayland toplevel lost pointer focus");
@@ -630,7 +651,17 @@ static void pointer_motion(void *data, struct wl_pointer *pointer,
(void)pointer;
if (!surface->pointer_focus) return;
if (surface->relative_motion_seen) return;
/* Compositors such as niri send wl_pointer.motion and relative motion
* together for every move while the pointer is unconstrained. Absolute
* coordinates are exact there, so prefer them whenever they change. Only
* stationary motion events, as sent while Wine has locked the pointer,
* defer to the relative deltas buffered for this frame. */
if (surface->last_motion_valid &&
x == surface->last_motion_x && y == surface->last_motion_y)
return;
surface->last_motion_x = x;
surface->last_motion_y = y;
surface->last_motion_valid = 1;
surface->pointer_x = wl_fixed_to_int(x);
surface->pointer_y = wl_fixed_to_int(y);
surface->frame.time = time;
@@ -681,6 +712,15 @@ static void pointer_frame(void *data, struct wl_pointer *pointer)
(void)pointer;
if (!surface->pointer_focus) return;
if (surface->relative_pending)
{
/* Relative deltas only drive the cursor when this frame carried no
* real absolute movement, i.e. while the game pointer is locked. */
if (!(surface->frame.flags & GE_STEAM_OVERLAY_FRAME_ABSOLUTE))
ge_overlay_bridge_filter_pointer_frame(&surface->relative_frame);
surface->relative_pending = 0;
memset(&surface->relative_frame, 0, sizeof(surface->relative_frame));
}
ge_overlay_bridge_filter_pointer_frame(&surface->frame);
memset(&surface->frame, 0, sizeof(surface->frame));
}
@@ -740,7 +780,6 @@ static void relative_pointer_motion(
wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel)
{
struct ge_overlay_wayland_surface *surface = data;
struct ge_steam_overlay_pointer_frame frame = {0};
uint64_t usec = ((uint64_t)utime_hi << 32) | utime_lo;
(void)relative_pointer;
@@ -749,26 +788,32 @@ static void relative_pointer_motion(
if (!surface->pointer_focus) return;
frame.time = (uint32_t)(usec / 1000);
/* Relative-pointer events continue while Wine has constrained the game
* pointer. Once they begin, use them for the remainder of this pointer
* focus. Some compositors also emit a stationary wl_pointer.motion event;
* using a time gate for that event can otherwise suppress every real
* relative delta and leave Steam's visible cursor frozen in place. */
* pointer. Buffer them until wl_pointer.frame: pointer_frame() only uses
* them when the same frame carried no real absolute movement, so an
* unconstrained pointer keeps its exact surface position. */
if (!surface->relative_motion_seen)
{
surface->relative_motion_seen = 1;
surface->frame.flags &= ~GE_STEAM_OVERLAY_FRAME_ABSOLUTE;
overlay_trace("relative-pointer motion is active");
}
frame.dx = wl_fixed_to_double(dx);
frame.dy = wl_fixed_to_double(dy);
frame.scale_x = surface->pointer_scale;
frame.scale_y = surface->pointer_scale;
frame.flags = GE_STEAM_OVERLAY_FRAME_RELATIVE;
ge_overlay_bridge_filter_pointer_frame(&frame);
/* Don't depend on wl_pointer.frame after relative-only motion: if earlier
* deltas are still buffered with no absolute motion since, apply them now
* so a compositor that skips the frame can't freeze the overlay cursor. */
if (surface->relative_pending && !(surface->frame.flags & GE_STEAM_OVERLAY_FRAME_ABSOLUTE))
{
ge_overlay_bridge_filter_pointer_frame(&surface->relative_frame);
memset(&surface->relative_frame, 0, sizeof(surface->relative_frame));
}
surface->relative_frame.time = (uint32_t)(usec / 1000);
surface->relative_frame.dx += wl_fixed_to_double(dx);
surface->relative_frame.dy += wl_fixed_to_double(dy);
surface->relative_frame.scale_x = surface->pointer_scale;
surface->relative_frame.scale_y = surface->pointer_scale;
surface->relative_frame.flags = GE_STEAM_OVERLAY_FRAME_RELATIVE;
surface->relative_pending = 1;
}
static const struct zwp_relative_pointer_v1_listener relative_pointer_listener =
@@ -1066,6 +1111,7 @@ void ge_overlay_wayland_surface_dispatch(struct ge_overlay_wayland_surface *surf
if (!surface || !surface->display || !surface->queue) return;
if (wl_display_dispatch_queue_pending(surface->display, surface->queue) < 0)
overlay_trace("failed to dispatch private Wayland input queue");
ge_overlay_bridge_pump();
}
void ge_overlay_wayland_surface_destroy(struct ge_overlay_wayland_surface *surface)
+69 -58
View File
@@ -2,6 +2,32 @@
WINE_DEFAULT_DEBUG_CHANNEL(steamclient);
static struct w_request *alloc_w_request(void)
{
struct w_request *request;
request = calloc( 1, sizeof(*request) );
request->magic = W_REQUEST_MAGIC;
return request;
}
static void free_w_request( struct w_request *request )
{
if (!request) return;
SecureZeroMemory( &request->magic, sizeof(request->magic) );
free( request );
}
static struct w_request *get_w_request( void *handle )
{
struct w_request *request = handle;
if (!request) return NULL;
if (request->magic == W_REQUEST_MAGIC) return request;
ERR( "Bad magic %#I64x.\n", request->magic );
return NULL;
}
void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers001_CancelQuery(struct w_iface *_this, uint32_t eType)
{
struct ISteamMatchmakingServers_SteamMatchMakingServers001_CancelQuery_params params =
@@ -24,17 +50,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestInternetServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -49,17 +74,15 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.iApp = iApp,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
TRACE( "%p, %p\n", _this, pRequestServersResponse );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestLANServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -76,17 +99,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestFriendsServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -103,17 +125,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestFavoritesServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -130,17 +151,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();;
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestHistoryServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -157,17 +177,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_RequestSpectatorServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -179,7 +198,7 @@ void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_CancelQue
struct ISteamMatchmakingServers_SteamMatchMakingServers002_CancelQuery_params params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_CancelQuery, &params );
@@ -191,17 +210,17 @@ void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_ReleaseRe
struct ISteamMatchmakingServers_SteamMatchMakingServers002_ReleaseRequest_params params =
{
.u_iface = _this->u_iface,
.hServerListRequest = hServerListRequest,
.hServerListRequest = get_w_request( hServerListRequest ),
};
struct w_request *request = hServerListRequest;
struct w_request *request = get_w_request( hServerListRequest );
TRACE( "%p %p\n", _this, hServerListRequest );
execute_pending_callbacks(); /* execute any pending callbacks that might still need to use the request */
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_ReleaseRequest, &params );
if (request) HeapFree( GetProcessHeap(), 0, request->details );
HeapFree( GetProcessHeap(), 0, request );
if (request) free( request->details );
free_w_request( request );
}
gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers002_GetServerDetails( struct w_iface *_this, void *hRequest, int32_t iServer )
@@ -209,10 +228,10 @@ gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingSe
struct ISteamMatchmakingServers_SteamMatchMakingServers002_GetServerDetails_params params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
.iServer = iServer,
};
struct w_request *request = hRequest;
struct w_request *request = get_w_request( hRequest );
TRACE( "%p\n", _this );
@@ -221,13 +240,12 @@ gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingSe
struct ISteamMatchmakingServers_SteamMatchMakingServers002_GetServerCount_params count_params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
};
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_GetServerCount, &count_params );
request->details_count = count_params._ret;
if (count_params._ret) request->details = HeapAlloc( GetProcessHeap(), 0,
(count_params._ret + 1) * sizeof(*request->details) );
if (count_params._ret) request->details = malloc( (count_params._ret + 1) * sizeof(*request->details) );
}
if (request && request->details && (iServer < 0 || iServer >= request->details_count))
{
@@ -257,17 +275,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestInternetServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -282,17 +299,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.iApp = iApp,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestLANServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -309,17 +325,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestFriendsServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -336,17 +351,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestFavoritesServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -363,17 +377,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestHistoryServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -390,17 +403,16 @@ void * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_Request
.nFilters = nFilters,
.pRequestServersResponse = pRequestServersResponse,
};
struct w_request *request;
struct w_request *request = alloc_w_request();
TRACE( "%p\n", _this );
if (!(request = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*request) ))) return NULL;
params._ret = request;
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_RequestSpectatorServerList, &params );
if (!request->u_request.handle)
{
HeapFree( GetProcessHeap(), 0, request );
free_w_request( request );
return NULL;
}
@@ -412,7 +424,7 @@ void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_CancelQue
struct ISteamMatchmakingServers_SteamMatchMakingServers003_CancelQuery_params params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_CancelQuery, &params );
@@ -424,17 +436,17 @@ void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_ReleaseRe
struct ISteamMatchmakingServers_SteamMatchMakingServers003_ReleaseRequest_params params =
{
.u_iface = _this->u_iface,
.hServerListRequest = hServerListRequest,
.hServerListRequest = get_w_request( hServerListRequest ),
};
struct w_request *request = hServerListRequest;
struct w_request *request = get_w_request( hServerListRequest );
TRACE( "%p %p\n", _this, hServerListRequest );
execute_pending_callbacks(); /* execute any pending callbacks that might still need to use the request */
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_ReleaseRequest, &params );
if (request) HeapFree( GetProcessHeap(), 0, request->details );
HeapFree( GetProcessHeap(), 0, request );
if (request) free( request->details );
free_w_request( request );
}
gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingServers003_GetServerDetails( struct w_iface *_this, void *hRequest, int32_t iServer )
@@ -442,10 +454,10 @@ gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingSe
struct ISteamMatchmakingServers_SteamMatchMakingServers003_GetServerDetails_params params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
.iServer = iServer,
};
struct w_request *request = hRequest;
struct w_request *request = get_w_request( hRequest );
TRACE( "%p\n", _this );
@@ -454,13 +466,12 @@ gameserveritem_t_105 * __thiscall winISteamMatchmakingServers_SteamMatchMakingSe
struct ISteamMatchmakingServers_SteamMatchMakingServers003_GetServerCount_params count_params =
{
.u_iface = _this->u_iface,
.hRequest = hRequest,
.hRequest = get_w_request( hRequest ),
};
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_GetServerCount, &count_params );
request->details_count = count_params._ret;
if (count_params._ret) request->details = HeapAlloc( GetProcessHeap(), 0,
(count_params._ret + 1) * sizeof(*request->details) );
if (count_params._ret) request->details = malloc( (count_params._ret + 1) * sizeof(*request->details) );
}
if (request && request->details && (iServer < 0 || iServer >= request->details_count))
{
+1
View File
@@ -111,6 +111,7 @@ extern NTSTATUS steamclient_networking_message_release_153a( void * );
extern NTSTATUS wow64_steamclient_networking_message_release_153a( void * );
extern char *steamclient_dos_to_unix_path( const char *src, int is_url );
extern char *steamclient_dos_to_unix_path_fixup_slash( const char *src, int is_url );
extern void steamclient_free_path( char *path_array );
extern const char **steamclient_dos_to_unix_path_array( const char **src_array );
extern const char **wow64_steamclient_dos_to_unix_path_array( ptr32< const char ** > src_array );
+31 -4
View File
@@ -1068,14 +1068,41 @@ char *steamclient_dos_to_unix_path( const char *src, int is_url )
}
done:
len = strlen( buffer ) + 1;
if (!(dst = (char *)malloc( len ))) return NULL;
memcpy( dst, buffer, len );
if (is_url || !IS_ABSOLUTE(src) || !(dst = realpath( buffer, NULL )))
{
len = strlen( buffer ) + 1;
if (!(dst = (char *)malloc( len ))) return NULL;
memcpy( dst, buffer, len );
}
TRACE( "-> %s\n", debugstr_a(dst) );
return dst;
}
char *steamclient_dos_to_unix_path_fixup_slash( const char *src, int is_url )
{
const char *install_dir_env;
char *path, *install_path, *p;
size_t len;
if (!(path = steamclient_dos_to_unix_path( src, is_url ))) return path;
if (!(install_dir_env = getenv( "STEAM_COMPAT_INSTALL_PATH" ))) return path;
if (!(install_path = realpath( install_dir_env, NULL ))) return path;
len = strlen( install_path );
if (!memcmp( path, install_path, len ))
{
p = path + len;
if (*p == '/') ++p;
while (*p)
{
if (*p == '/') *p = '\\';
++p;
}
}
free( install_path );
TRACE( "-> %s\n", debugstr_a(path) );
return path;
}
void steamclient_free_path( char *path )
{
free( path );
+3
View File
@@ -412,8 +412,11 @@ struct networking_message
#endif /* __cplusplus */
};
#define W_REQUEST_MAGIC 0xe802211e9c94aaff
struct w_request
{
UINT64 magic;
struct u_request u_request;
struct u_response u_response;
union
@@ -1,16 +0,0 @@
Allow AVX builds for Windows with clang.
The check exists because GCC does not realign the stack for 32-byte spills on
Windows. Clang does.
--- a/src/util/util_bit.h
+++ b/src/util/util_bit.h
@@ -12,7 +12,7 @@
#ifdef DXVK_ARCH_X86
#ifndef _MSC_VER
- #if defined(_WIN32) && (defined(__AVX__) || defined(__AVX2__))
+ #if !defined(__clang__) && defined(_WIN32) && (defined(__AVX__) || defined(__AVX2__))
#error "AVX-enabled builds not supported due to stack alignment issues."
#endif
#if defined(__WINE__) && defined(__clang__)
@@ -0,0 +1,51 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] d3d9: Disable constant buffer streaming for StarCraft II on
Navi 10 only.
StarCraft II intermittently hung the GPU on RADV with an RX 5700 XT
(Navi 10), and d3d9.deviceLocalConstantBuffers = False avoided it (#790).
That was applied as an application profile for every GPU, but on an
RX 7900 GRE the same setting makes the game freeze during coop, reliably
across several sessions, while Auto does not (#845).
Only default to False for SC2.exe and SC2_x64.exe on AMD Navi 10 device
IDs (0x7310-0x731f), and only while the option is Auto, so setting it
to True or False still wins. Other GPUs keep DXVK's default.
The underlying failure has not been bisected.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/790
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/845
---
--- a/src/d3d9/d3d9_options.cpp
+++ b/src/d3d9/d3d9_options.cpp
@@ -1,3 +1,6 @@
+#include <cctype>
+
+#include "../util/util_env.h"
#include "../util/util_math.h"
#include "d3d9_options.h"
@@ -66,6 +69,23 @@
this->enumerateByDisplays = config.getOption<bool> ("d3d9.enumerateByDisplays", true);
this->cachedWriteOnlyBuffers = config.getOption<bool> ("d3d9.cachedWriteOnlyBuffers", false);
this->deviceLocalConstantBuffers = config.getOption<Tristate> ("d3d9.deviceLocalConstantBuffers", Tristate::Auto);
+
+ /* StarCraft II hangs the GPU with constant buffer streaming on an RX 5700 XT
+ * (Navi 10, GE #790), but disabling it freezes the game in coop on an
+ * RX 7900 GRE (GE #845), so only disable it on Navi 10. Setting
+ * d3d9.deviceLocalConstantBuffers to True or False still wins. */
+ if (this->deviceLocalConstantBuffers == Tristate::Auto && adapter != nullptr) {
+ std::string exe = env::getExeName();
+ const auto info = adapter->info();
+
+ for (auto& c : exe)
+ c = std::tolower(static_cast<unsigned char>(c));
+
+ if ((exe == "sc2.exe" || exe == "sc2_x64.exe")
+ && info.vendorId == 0x1002 && info.deviceId >= 0x7310 && info.deviceId <= 0x731f)
+ this->deviceLocalConstantBuffers = Tristate::False;
+ }
+
this->allowDirectBufferMapping = config.getOption<bool> ("d3d9.allowDirectBufferMapping", true);
this->forceDrawTimeBufferUpload = config.getOption<bool> ("d3d9.forceDrawTimeBufferUpload", false);
this->ignoreDefaultBufferLockRange = config.getOption<bool> ("d3d9.ignoreDefaultBufferLockRange", false);
@@ -1,31 +0,0 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] d3d9: Disable constant buffer streaming for StarCraft II.
StarCraft II intermittently hangs the GPU on RADV with an RX 5700 XT
using GE-Proton11-7. Replacing only DXVK with the GE-Proton10-34 version
avoids the failure. Disabling descriptor heaps and descriptor buffers was
not sufficient, but the reporter now reports success with only
d3d9.deviceLocalConstantBuffers = False on the newer build.
Disable constant buffer streaming in the application profile for SC2.exe
and SC2_x64.exe. Leave descriptor selection, Battle.net, other games and
the global default unchanged. User configuration can still override this
application default. This is a targeted compatibility workaround; the
underlying DXVK/driver failure has not been bisected to a specific commit.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/790
---
diff --git a/src/util/config/config.cpp b/src/util/config/config.cpp
--- a/src/util/config/config.cpp
+++ b/src/util/config/config.cpp
@@ -534,6 +534,10 @@
}} },
+ /* StarCraft II: avoid GPU hangs with constant buffer streaming. */
+ { R"(\\SC2(_x64)?\.exe$)", {{
+ { "d3d9.deviceLocalConstantBuffers", "False" },
+ }} },
/* A Hat in Time */
{ R"(\\HatinTimeGame\.exe$)", {{
{ "d3d9.lenientClear", "True" },
}} },
@@ -0,0 +1,89 @@
From: Mihnea Gîrbăcică (Mihnea8848)
Subject: [PATCH] winex11: let ALPHA-mode layered overlays present directly
With WINE_LAYERED_OVERLAY_ALPHA=1 every WS_EX_LAYERED window renders offscreen
and each frame is copied to the ARGB window afterwards. The copy runs right
after vkQueuePresentKHR returns, before the driver's present has landed in the
offscreen window, so it picks up the new frame on some presents and the old one
on others: about a third of the frames are shown twice and others never
(Desktop Mate: 60 frames rendered, about 45 distinct frames on screen).
WINE_LAYERED_OVERLAY_DIRECT=1 skips the forced offscreen rendering, so the
swapchain presents straight into the ARGB window. The input shape is then
sampled on present from the toplevel's composite pixmap, which also works for
windows larger than the root. Without the variable nothing changes.
diff --git a/dlls/winex11.drv/init.c b/dlls/winex11.drv/init.c
--- a/dlls/winex11.drv/init.c
+++ b/dlls/winex11.drv/init.c
@@ -297,6 +297,19 @@
return enabled;
}
+/* WINE_LAYERED_OVERLAY_DIRECT=1 lets ALPHA-mode windows present straight into the ARGB
+ * window instead of rendering offscreen and copying every frame */
+static BOOL layered_overlay_direct_enabled(void)
+{
+ static int enabled = -1;
+ if (enabled == -1)
+ {
+ const char *e = getenv( "WINE_LAYERED_OVERLAY_DIRECT" );
+ enabled = e && atoi( e );
+ }
+ return enabled;
+}
+
/* minimum ms between ALPHA-mode input-shape samples; 0 samples on every present */
static DWORD layered_overlay_shape_interval(void)
{
@@ -331,7 +344,8 @@
needs_offscreen = TRUE;
/* Layered overlay (DWM-glass style) painting per-pixel alpha via a 3D client surface. */
- if (!needs_offscreen && (layered_overlay_shape_enabled() || layered_overlay_alpha_enabled()) && (ex_style & WS_EX_LAYERED))
+ if (!needs_offscreen && (layered_overlay_shape_enabled() || layered_overlay_alpha_enabled()) && (ex_style & WS_EX_LAYERED) &&
+ !(layered_overlay_alpha_enabled() && layered_overlay_direct_enabled()))
needs_offscreen = TRUE;
if (!needs_offscreen && (surface = window_surface_get( hwnd )))
@@ -704,6 +718,7 @@
{
Drawable drawable = surface->window;
Pixmap pixmap = 0;
+ int src_x = 0, src_y = 0;
BOOL got;
X11DRV_expect_error( gdi_display, overlay_xshm_error, NULL );
@@ -713,8 +728,15 @@
redirected pixmap holds the same pixels and has no on-screen check, so read that. */
if (usexcomposite && surface->client.offscreen)
drawable = pixmap = pXCompositeNameWindowPixmap( gdi_display, surface->window );
+ /* presenting directly: read the toplevel's pixmap (Xwayland redirects toplevels) */
+ else if (usexcomposite && alpha_mode && layered_overlay_direct_enabled())
+ {
+ drawable = pixmap = pXCompositeNameWindowPixmap( gdi_display, window );
+ src_x = surface->changes.x;
+ src_y = surface->changes.y;
+ }
#endif
- got = XShmGetImage( gdi_display, drawable, surface->shm_image, 0, 0, AllPlanes );
+ got = XShmGetImage( gdi_display, drawable, surface->shm_image, src_x, src_y, AllPlanes );
if (pixmap)
{
XFreePixmap( gdi_display, pixmap );
@@ -848,7 +870,14 @@
client_surface_update_geometry( hwnd, surface );
client_surface_update_offscreen( hwnd, surface );
- if (!hdc) return;
+ if (!hdc)
+ {
+ /* direct present: nothing to copy, but the input shape still follows the pixels */
+ if (layered_overlay_alpha_enabled() && layered_overlay_direct_enabled() &&
+ (NtUserGetWindowLongW( toplevel, GWL_EXSTYLE ) & WS_EX_LAYERED))
+ update_layered_overlay_shape( surface, toplevel );
+ return;
+ }
if (hwnd && (win_surface = window_surface_get( hwnd )))
{
@@ -24,7 +24,12 @@ readback drives the visible bounding shape. WINE_LAYERED_OVERLAY_INPUT_SHAPE=0 s
shape, and the readback that feeds it, altogether: with it off the window keeps the stock
empty-input-shape pass-through rather than falling back to taking input over its whole
rectangle. 1080p full-screen overlay on XWayland: 11.26 -> 1.16 ms per present, with a
bit-identical block mask.
bit-identical block mask. The offscreen client window is parked at the origin of the dummy
parent, so XShmGetImage on it fails with BadMatch as soon as it is wider or taller than the
root window; for that window the readback goes through its composite pixmap instead, which
holds the same pixels and keeps the MIT-SHM path working at any window size. If the fast read
fails anyway (no XComposite, a window that is not redirected), the surface stays on XGetImage
until it is resized instead of raising BadMatch on every sample.
Based on the original TBH-specific XShape patch by Solarisfire; generalised, flicker fixed,
extended with the real-alpha (ARGB) path and per-pixel input shaping.
@@ -139,7 +144,7 @@ index 614450e72d6..8cb0f0d2b52 100644
if (!needs_offscreen && (surface = window_surface_get( hwnd )))
{
TRACE("hwnd %p, surface %p, surface->alpha_mask %#x.\n", hwnd, surface, surface->alpha_mask);
@@ -321,6 +393,22 @@ struct x11drv_client_surface
@@ -321,6 +393,23 @@ struct x11drv_client_surface
HDC hdc_src;
HDC hdc_dst;
BOOL other_process;
@@ -157,6 +162,7 @@ index 614450e72d6..8cb0f0d2b52 100644
+ XShmSegmentInfo shm_info;
+ BOOL shm_active;
+ unsigned int shm_width, shm_height;
+ BOOL shm_broken; /* the fast read failed; use XGetImage until resized */
+#endif
+ DWORD shape_last_sample;
};
@@ -179,7 +185,7 @@ index 614450e72d6..8cb0f0d2b52 100644
}
static void x11drv_client_surface_detach( struct client_surface *client )
@@ -465,6 +563,265 @@ static void x11drv_client_surface_update( struct client_surface *client )
@@ -465,6 +564,282 @@ static void x11drv_client_surface_update( struct client_surface *client )
client_surface_update_offscreen( hwnd, surface );
}
@@ -315,19 +321,36 @@ index 614450e72d6..8cb0f0d2b52 100644
+ free( surface->shape_rects );
+ surface->shape_rects = NULL;
+ surface->shape_rect_count = 0;
+#ifdef HAVE_LIBXXSHM
+ surface->shm_broken = FALSE;
+#endif
+ }
+ if (!surface->shape_age) return TRUE;
+
+ /* MIT-SHM avoids copying the whole image over the X socket */
+#ifdef HAVE_LIBXXSHM
+ if (ensure_shm_image( surface, width, height ))
+ if (!surface->shm_broken && ensure_shm_image( surface, width, height ))
+ {
+ Drawable drawable = surface->window;
+ Pixmap pixmap = 0;
+ BOOL got;
+
+ /* a window reaching past the root window fails here, but not in XGetImage */
+ X11DRV_expect_error( gdi_display, overlay_xshm_error, NULL );
+ got = XShmGetImage( gdi_display, surface->window, surface->shm_image, 0, 0, AllPlanes );
+#ifdef SONAME_LIBXCOMPOSITE
+ /* The offscreen client window sits at the origin of the dummy parent, so XShmGetImage
+ fails on it with BadMatch once it is wider or taller than the root window. Its
+ redirected pixmap holds the same pixels and has no on-screen check, so read that. */
+ if (usexcomposite && surface->client.offscreen)
+ drawable = pixmap = pXCompositeNameWindowPixmap( gdi_display, surface->window );
+#endif
+ got = XShmGetImage( gdi_display, drawable, surface->shm_image, 0, 0, AllPlanes );
+ if (pixmap)
+ {
+ XFreePixmap( gdi_display, pixmap );
+ XSync( gdi_display, False );
+ }
+ if (X11DRV_check_error()) got = FALSE;
+ if (!got) surface->shm_broken = TRUE;
+ if (got)
+ {
+ image = surface->shm_image; /* owned by the surface */
@@ -445,7 +468,7 @@ index 614450e72d6..8cb0f0d2b52 100644
static void X11DRV_client_surface_present( struct client_surface *client, HDC hdc )
{
struct x11drv_client_surface *surface = impl_from_client_surface( client );
@@ -493,8 +850,12 @@ static void X11DRV_client_surface_present( struct client_surface *client, HDC hd
@@ -493,8 +863,12 @@ static void X11DRV_client_surface_present( struct client_surface *client, HDC hd
TRACE( "Surface is present.\n" );
region = get_dc_monitor_region( hwnd, hdc );
if (region) NtGdiExtSelectClipRgn( hdc, region, RGN_COPY );
@@ -460,7 +483,7 @@ index 614450e72d6..8cb0f0d2b52 100644
if (region) NtGdiDeleteObjectApp( region );
window_surface_release( win_surface );
return;
@@ -527,8 +888,9 @@ static void X11DRV_client_surface_present( struct client_surface *client, HDC hd
@@ -527,8 +901,9 @@ static void X11DRV_client_surface_present( struct client_surface *client, HDC hd
set_dc_drawable( surface->hdc_dst, window, &rect_dst, IncludeInferiors );
if (region) NtGdiExtSelectClipRgn( surface->hdc_dst, region, RGN_COPY );
@@ -472,7 +495,7 @@ index 614450e72d6..8cb0f0d2b52 100644
XFlush( gdi_display );
done:
@@ -570,6 +932,22 @@ Window x11drv_client_surface_create( HWND hwnd, BOOL raw, int format, struct cli
@@ -570,6 +945,22 @@ Window x11drv_client_surface_create( HWND hwnd, BOOL raw, int format, struct cli
if (format && !visual_from_pixel_format( format, &visual )) return None;
@@ -14079,7 +14079,7 @@ index 649cd4d044bdea1f35ff082654eaff97b751ea8f..7124ff0e446a61c42dbfe7772c70b615
}
else
{
@@ -993,16 +1593,25 @@ static HRESULT WINAPI video_processor_ProcessOutput(IMFTransform *iface, DWORD f
@@ -993,16 +1593,22 @@ static HRESULT WINAPI video_processor_ProcessOutput(IMFTransform *iface, DWORD f
return E_INVALIDARG;
}
IMFSample_AddRef(output_sample);
@@ -14088,9 +14088,6 @@ index 649cd4d044bdea1f35ff082654eaff97b751ea8f..7124ff0e446a61c42dbfe7772c70b615
- if (FAILED(hr = video_processor_process_output_d3d11(impl, input_sample, output_sample)))
+ if (use_d3d11_processor)
+ use_d3d11_processor = FALSE;
+
+ if (use_d3d11_processor)
+ hr = video_processor_process_output_d3d11(impl, input_sample, output_sample);
+ else
+ hr = video_processor_process_output_cpu(impl, input_sample, output_sample, samples);
@@ -14110,7 +14107,7 @@ index 649cd4d044bdea1f35ff082654eaff97b751ea8f..7124ff0e446a61c42dbfe7772c70b615
if (provide_samples)
{
@@ -1123,29 +1732,11 @@ static const IMFVideoProcessorControlVtbl video_processor_control_vtbl =
@@ -1123,29 +1729,11 @@ static const IMFVideoProcessorControlVtbl video_processor_control_vtbl =
HRESULT video_processor_create(REFIID riid, void **ret)
{
@@ -14140,7 +14137,7 @@ index 649cd4d044bdea1f35ff082654eaff97b751ea8f..7124ff0e446a61c42dbfe7772c70b615
if (!(impl = calloc(1, sizeof(*impl))))
return E_OUTOFMEMORY;
@@ -1158,8 +1749,6 @@ HRESULT video_processor_create(REFIID riid, void **ret)
@@ -1158,8 +1746,6 @@ HRESULT video_processor_create(REFIID riid, void **ret)
goto failed;
if (FAILED(hr = MFCreateAttributes(&impl->output_attributes, 0)))
goto failed;
@@ -0,0 +1,263 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: AITUS95 <[email protected]>
Date: Sat, 31 Oct 2026 23:00:00 +0100
Subject: [PATCH] ddraw: fix The Godfather FMV flicker and edge flash
The Godfather (2006) draws YUY2 DirectDraw videos alongside D3D9.
Use a layered overlay window to avoid flicker and show it only after
its first blit to prevent the right-edge flash. Restore the original
swapchain window when video playback ends.
Apply only to the game's executables. GF_DDRAW_DISABLE=1 disables the
workaround if necessary. Other games keep the existing DirectDraw path.
Tested on Ubuntu 26.04 with GE-Proton, NVIDIA and a Wayland desktop.
The other Proton rendering backends did not fix the original flicker.
---
--- a/dlls/ddraw/ddraw.c
+++ b/dlls/ddraw/ddraw.c
@@ -374,6 +374,8 @@
{
unsigned int i;
+ ddraw_gf_close_overlay_window(ddraw);
+
TRACE("Destroying the swapchain.\n");
wined3d_swapchain_decref(ddraw->wined3d_swapchain);
@@ -5060,8 +5062,10 @@
r = &output_desc.desktop_rect;
TRACE("Resizing window %p to %s.\n", ddraw->swapchain_window, wine_dbgstr_rect(r));
- if (!SetWindowPos(ddraw->swapchain_window, HWND_TOP, r->left, r->top,
- r->right - r->left, r->bottom - r->top, SWP_SHOWWINDOW | SWP_NOACTIVATE))
+ /* The temporary video window is not the fullscreen device window. */
+ if (!ddraw->gf_overlay_window
+ && !SetWindowPos(ddraw->swapchain_window, HWND_TOP, r->left, r->top,
+ r->right - r->left, r->bottom - r->top, SWP_SHOWWINDOW | SWP_NOACTIVATE))
ERR("Failed to resize window.\n");
InterlockedCompareExchange(&ddraw->device_state, DDRAW_DEVICE_STATE_NOT_RESTORED, DDRAW_DEVICE_STATE_OK);
--- a/dlls/ddraw/ddraw_private.h
+++ b/dlls/ddraw/ddraw_private.h
@@ -111,6 +111,7 @@
* destination is presented. */
struct ddraw_surface *overlay, *overlay_dst;
RECT overlay_src_rect, overlay_dst_rect;
+ HWND gf_overlay_window, gf_previous_window;
struct wined3d_texture *gdi_surface;
struct wined3d_swapchain *wined3d_swapchain;
struct wined3d_swapchain_state_parent state_parent;
@@ -153,6 +154,8 @@
HRESULT ddraw_get_d3dcaps(const struct ddraw *ddraw, D3DDEVICEDESC7 *caps);
void ddraw_update_lost_surfaces(struct ddraw *ddraw);
+void ddraw_gf_close_overlay_window(struct ddraw *ddraw);
+
static inline void ddraw_set_swapchain_window(struct ddraw *ddraw, HWND window)
{
if (window == GetDesktopWindow())
--- a/dlls/ddraw/surface.c
+++ b/dlls/ddraw/surface.c
@@ -145,7 +145,8 @@
* the time. Testing the game Windows 98 shows this exact behaviour; the
* buttons often fail to highlight correctly when you mouse over them. */
if (ddraw->swapchain_window && (!(ddraw->cooperative_level & DDSCL_EXCLUSIVE)
- || ddraw->swapchain_window == GetForegroundWindow() || ddraw_gdi_is_front(ddraw)))
+ || ddraw->gf_overlay_window || ddraw->swapchain_window == GetForegroundWindow()
+ || ddraw_gdi_is_front(ddraw)))
{
/* Nothing to do, we control the frontbuffer, or at least the parts we
* care about. */
@@ -4163,6 +4164,154 @@
/* Wine has no hardware overlays: the visible overlay is drawn over its
* destination when that is presented (see ddraw_surface_update_frontbuffer()).
* Only one overlay is shown at a time. */
+/* The Godfather uses DirectDraw overlays alongside D3D9 presents. */
+static BOOL ddraw_gf_overlay_enabled(void)
+{
+ static LONG cached = -1;
+ WCHAR path[MAX_PATH];
+ const WCHAR *filename;
+ char value[4];
+ DWORD len;
+ LONG match;
+ unsigned int i;
+
+ if (GetEnvironmentVariableA("GF_DDRAW_DISABLE", value, sizeof(value)) == 1
+ && value[0] == '1')
+ return FALSE;
+
+ if ((match = InterlockedCompareExchange(&cached, -1, -1)) != -1)
+ return match;
+
+ match = 0;
+ len = GetModuleFileNameW(NULL, path, ARRAY_SIZE(path));
+ if (len && len < ARRAY_SIZE(path))
+ {
+ filename = path;
+ for (i = 0; i < len; ++i)
+ if (path[i] == '\\' || path[i] == '/')
+ filename = path + i + 1;
+
+ match = !lstrcmpiW(filename, L"godfather.exe")
+ || !lstrcmpiW(filename, L"The Godfather The Game.exe");
+ }
+
+ InterlockedCompareExchange(&cached, match, -1);
+ return match;
+}
+
+void ddraw_gf_close_overlay_window(struct ddraw *ddraw)
+{
+ HWND window = ddraw->gf_overlay_window;
+ HWND previous = ddraw->gf_previous_window;
+
+ if (!window)
+ return;
+
+ if (!previous)
+ previous = ddraw->dest_window;
+
+ ddraw->gf_overlay_window = NULL;
+ ddraw->gf_previous_window = NULL;
+
+ if (ddraw->wined3d_swapchain && previous && IsWindow(previous))
+ {
+ wined3d_swapchain_set_window(ddraw->wined3d_swapchain, previous);
+ ddraw_set_swapchain_window(ddraw, previous);
+ }
+
+ if (IsWindow(window) && !DestroyWindow(window))
+ {
+ DWORD error = GetLastError();
+ ShowWindow(window, SW_HIDE);
+ TRACE("GF-DDRAW-WINDOW: DestroyWindow failed, error %lu.\n", error);
+ }
+
+ TRACE("GF-DDRAW-WINDOW: closed.\n");
+}
+
+static void ddraw_gf_prepare_overlay_window(struct ddraw *ddraw)
+{
+ HWND window;
+ RECT client, current;
+ POINT origin = {0, 0};
+ int width, height;
+
+ if (!ddraw_gf_overlay_enabled()
+ || !(ddraw->cooperative_level & DDSCL_EXCLUSIVE)
+ || !ddraw->wined3d_swapchain
+ || !ddraw->dest_window
+ || !IsWindow(ddraw->dest_window)
+ || !IsWindowVisible(ddraw->dest_window))
+ return;
+
+ if (!GetClientRect(ddraw->dest_window, &client)
+ || !ClientToScreen(ddraw->dest_window, &origin))
+ return;
+
+ width = client.right - client.left;
+ height = client.bottom - client.top;
+ if (width <= 0 || height <= 0)
+ return;
+
+ window = ddraw->gf_overlay_window;
+ /* Recover from unexpected destruction of the transient popup. */
+ if (window && !IsWindow(window))
+ {
+ ddraw_gf_close_overlay_window(ddraw);
+ window = NULL;
+ }
+
+ if (!window)
+ {
+ if (ddraw->swapchain_window != ddraw->dest_window)
+ {
+ TRACE("GF-DDRAW-WINDOW: unexpected swapchain window %p, game %p.\n",
+ ddraw->swapchain_window, ddraw->dest_window);
+ return;
+ }
+
+ window = CreateWindowExA(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED,
+ DDRAW_WINDOW_CLASS_NAME, "GF DirectDraw Overlay", WS_POPUP,
+ origin.x, origin.y, width, height, ddraw->dest_window, NULL, NULL, NULL);
+ if (!window)
+ {
+ TRACE("GF-DDRAW-WINDOW: creation failed, error %lu.\n", GetLastError());
+ return;
+ }
+
+ if (!SetLayeredWindowAttributes(window, 0, 254, LWA_ALPHA))
+ {
+ TRACE("GF-DDRAW-WINDOW: layered attributes failed, error %lu.\n", GetLastError());
+ DestroyWindow(window);
+ return;
+ }
+
+ if (!SetWindowPos(window, HWND_TOPMOST, origin.x, origin.y,
+ width, height, SWP_NOACTIVATE))
+ {
+ TRACE("GF-DDRAW-WINDOW: positioning failed, error %lu.\n", GetLastError());
+ DestroyWindow(window);
+ return;
+ }
+
+ ddraw->gf_previous_window = ddraw->swapchain_window;
+ ddraw->gf_overlay_window = window;
+ wined3d_swapchain_set_window(ddraw->wined3d_swapchain, window);
+ ddraw_set_swapchain_window(ddraw, window);
+
+ TRACE("GF-DDRAW-WINDOW: opened %p at %ld,%ld size %dx%d opacity=254/255.\n",
+ window, origin.x, origin.y, width, height);
+ }
+ else if (GetWindowRect(window, &current)
+ && (current.left != origin.x || current.top != origin.y
+ || current.right - current.left != width
+ || current.bottom - current.top != height))
+ {
+ SetWindowPos(window, HWND_TOPMOST, origin.x, origin.y,
+ width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW);
+ }
+}
+
static void ddraw_surface_set_visible_overlay(struct ddraw_surface *overlay, const RECT *src_rect,
struct ddraw_surface *dst, const RECT *dst_rect, DWORD flags)
{
@@ -4176,6 +4325,7 @@
{
if (ddraw->overlay != overlay)
return;
+ ddraw_gf_close_overlay_window(ddraw);
old_dst = ddraw->overlay_dst;
old_dst_rect = ddraw->overlay_dst_rect;
ddraw->overlay = ddraw->overlay_dst = NULL;
@@ -4221,7 +4371,17 @@
else
SetRect(&ddraw->overlay_dst_rect, 0, 0, dst->surface_desc.dwWidth, dst->surface_desc.dwHeight);
+ ddraw_gf_prepare_overlay_window(ddraw);
ddraw_surface_update_overlay_dst(ddraw);
+
+ if (ddraw->gf_overlay_window && !IsWindowVisible(ddraw->gf_overlay_window))
+ {
+ if (!SetWindowPos(ddraw->gf_overlay_window, HWND_TOPMOST, 0, 0, 0, 0,
+ SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW))
+ TRACE("GF-DDRAW-DEFER-SHOW: show failed, error %lu.\n", GetLastError());
+ else
+ TRACE("GF-DDRAW-DEFER-SHOW: first blit submitted before show.\n");
+ }
}
/*****************************************************************************
@@ -6336,7 +6496,10 @@
}
if (surface == surface->ddraw->overlay || surface == surface->ddraw->overlay_dst)
+ {
+ ddraw_gf_close_overlay_window(surface->ddraw);
surface->ddraw->overlay = surface->ddraw->overlay_dst = NULL;
+ }
wined3d_private_store_cleanup(&surface->private_store);
@@ -0,0 +1,52 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Fri, 23 Oct 2026 18:00:00 -0600
Subject: [PATCH] winedmo: Complete audio MFT drain with NEED_MORE_INPUT
Once a drained transform has delivered all of its output, ProcessOutput is
expected to return MF_E_TRANSFORM_NEED_MORE_INPUT. The winedmo AAC and WMA
decoders instead translated the Unix decoder's terminal STATUS_END_OF_FILE
into MF_E_END_OF_STREAM.
Warrior Cats: Clans of the Forest plays its intro videos through Unreal's
ElectraPlayer. The video-only clips finish, but playback stops on the first
clip with an AAC track after both decoders reach end of stream, and the game
never reaches the main menu.
Apply the fix made to the video decoder in 0023 to the audio decoders too,
matching the MFT contract and the previous WineGStreamer decoder behavior.
Fixes: https://github.com/GloriousEggroll/proton-ge-custom/issues/882
---
dlls/winedmo/audio_decoder.c | 2 +-
dlls/winedmo/wma_decoder.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/dlls/winedmo/audio_decoder.c b/dlls/winedmo/audio_decoder.c
index 19a6beb..3168a86 100644
--- a/dlls/winedmo/audio_decoder.c
+++ b/dlls/winedmo/audio_decoder.c
@@ -750,7 +750,7 @@ static HRESULT WINAPI transform_ProcessOutput(IMFTransform *iface, DWORD flags,
else if (ntstatus == STATUS_END_OF_FILE)
{
samples->dwStatus = MFT_OUTPUT_DATA_BUFFER_NO_SAMPLE;
- return MF_E_END_OF_STREAM;
+ return MF_E_TRANSFORM_NEED_MORE_INPUT;
}
else
{
diff --git a/dlls/winedmo/wma_decoder.c b/dlls/winedmo/wma_decoder.c
index 368e7c6..6c75133 100644
--- a/dlls/winedmo/wma_decoder.c
+++ b/dlls/winedmo/wma_decoder.c
@@ -743,7 +743,7 @@ retry:
else if (ntstatus == STATUS_END_OF_FILE)
{
samples->dwStatus = MFT_OUTPUT_DATA_BUFFER_NO_SAMPLE;
- hr = MF_E_END_OF_STREAM;
+ hr = MF_E_TRANSFORM_NEED_MORE_INPUT;
}
else
{
--
2.51.0
@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Sat, 24 Oct 2026 05:00:00 +0200
Subject: [PATCH] quartz: Don't stop a graph run since StopWhenReady().
When the graph is not cued at once, StopWhenReady() leaves the stop to a
thread pool callback, which waits up to 100 ms for the pause to complete
and then stops the graph unconditionally. A Run() called meanwhile does
not cancel it: the graph is stopped under the application, which thinks
it is playing.
Silent Hill: The Arcade (Konami) renders each movie, cues it with
StopWhenReady() and runs it right away: the callback stopped the graph
about a millisecond after the Run(), and every movie stayed black (its
audio and subtitles, driven by the game, went on).
Check the graph is still paused before stopping it: a Run() or Stop()
issued since StopWhenReady() supersedes it.
---
diff --git a/dlls/quartz/filtergraph.c b/dlls/quartz/filtergraph.c
index 5f7c2ae7..2c6dec55 100644
--- a/dlls/quartz/filtergraph.c
+++ b/dlls/quartz/filtergraph.c
@@ -2397,12 +2397,26 @@ static HRESULT WINAPI MediaControl_get_RegFilterCollection(IMediaControl *iface,
static void CALLBACK wait_pause_cb(TP_CALLBACK_INSTANCE *instance, void *context)
{
IMediaControl *control = context;
+ struct filter_graph *graph = impl_from_IMediaControl(control);
OAFilterState state;
+ BOOL paused;
HRESULT hr;
if ((hr = IMediaControl_GetState(control, 100, &state)) != S_OK && hr != VFW_S_STATE_INTERMEDIATE)
ERR("Failed to get paused state, hr %#lx.\n", hr);
+ /* Run() or Stop() called since StopWhenReady() supersede it: a graph run meanwhile keeps
+ * running (Silent Hill: The Arcade runs its movie right after StopWhenReady()). */
+ EnterCriticalSection(&graph->cs);
+ paused = graph->state == State_Paused;
+ LeaveCriticalSection(&graph->cs);
+ if (!paused)
+ {
+ TRACE("Graph %p left the paused state, not stopping it.\n", graph);
+ IMediaControl_Release(control);
+ return;
+ }
+
if (FAILED(hr = IMediaControl_Stop(control)))
ERR("Failed to stop, hr %#lx.\n", hr);
@@ -0,0 +1,53 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Sat, 24 Oct 2026 05:00:00 +0200
Subject: [PATCH] winedmo: Seek the DirectShow parser's demuxer to the start
position on every start.
dmo_parser_init_stream() seeks the demuxer to the current position only
when it is not 0, assuming position 0 means the demuxer is still at the
start of the file. It is not once the filter has streamed: a pause
(preroll, StopWhenReady()) or a previous run makes the demux thread read
ahead, and a stop followed by SetPositions(0) only records the position
while the filter is stopped. The next run then resumes from wherever the
demux thread had read to.
With a raw MPEG-2 video stream (Silent Hill: The Arcade's movies, one
sequence header at the start of each file) the decoder then never gets
the sequence header and decodes nothing: "Invalid frame dimensions 0x0".
Remember that the filter has streamed and seek to position 0 too after
that.
---
diff --git a/dlls/winedmo/winedmo_quartz_parser.c b/dlls/winedmo/winedmo_quartz_parser.c
index 10895feb..0a311fd6 100644
--- a/dlls/winedmo/winedmo_quartz_parser.c
+++ b/dlls/winedmo/winedmo_quartz_parser.c
@@ -234,6 +234,8 @@ struct dmo_parser
REFERENCE_TIME stream_start;
HANDLE demux_thread;
+ /* The filter has streamed once: its demuxer has read ahead, it is not at the start. */
+ bool demuxer_moved;
BOOL (*init_streams)(struct dmo_parser *filter, UINT stream_count, INT64 duration);
BOOL (*check_mime_type)(const WCHAR *mime_type);
@@ -1577,7 +1579,9 @@ static HRESULT dmo_parser_init_stream(struct strmbase_filter *iface)
/* Seek to the current position (retain seek state across Stop/Pause/Run). */
{
LONGLONG seek_time = filter->source_count ? filter->sources[0]->seek.llCurrent : 0;
- if (seek_time)
+ /* After a previous run (or a pause that prerolled), the demuxer has read ahead:
+ * position 0 needs a seek too. */
+ if (seek_time || filter->demuxer_moved)
{
NTSTATUS status;
WARN("DirectShow parser init seek to %s.\n", debugstr_time(seek_time));
@@ -1590,6 +1594,7 @@ static HRESULT dmo_parser_init_stream(struct strmbase_filter *iface)
filter->streaming = true;
filter->flushing = false;
filter->read_flushing = FALSE;
+ filter->demuxer_moved = true;
for (i = 0; i < filter->source_count; ++i)
{
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Sat, 24 Oct 2026 05:00:00 +0200
Subject: [PATCH] winedmo: Drop an MPEG elementary stream picture the decoder
rejects.
Without an FFmpeg MPEG video parser, transform_push_input() assembles
MPEG-1/2 elementary stream input into pictures itself. When
avcodec_send_packet() rejected a picture, it returned an error before
removing the picture from its buffer: every following input sent the
same picture again, which failed again, and the stream never recovered
(one "avcodec_send_packet" warning per input sample, no frame).
Drop the rejected picture and go on with the next one, the decoder
resynchronizes at the next decodable picture.
---
diff --git a/dlls/winedmo/unix_transform.c b/dlls/winedmo/unix_transform.c
index af6173d1..03e51cc2 100644
--- a/dlls/winedmo/unix_transform.c
+++ b/dlls/winedmo/unix_transform.c
@@ -1279,8 +1279,9 @@ NTSTATUS transform_push_input( void *arg )
if ((ret = avcodec_send_packet( t->avctx, t->parsed_packet )) < 0)
{
if (ret == AVERROR(EAGAIN)) return STATUS_DEVICE_BUSY;
- WARN( "avcodec_send_packet: %d\n", ret );
- return STATUS_UNSUCCESSFUL;
+ /* Drop the rejected picture: kept, it would be sent again with every input and
+ * fail for ever (e.g. a stream entered without its sequence header). */
+ WARN( "avcodec_send_packet: %d, dropping %u bytes\n", ret, next_pic );
}
memmove( t->mpeg_es_buf, t->mpeg_es_buf + next_pic, t->mpeg_es_size - next_pic );
@@ -0,0 +1,105 @@
From: Alexandre Derumier <[email protected]>
Date: Fri, 23 Oct 2026 16:00:00 +0200
Subject: [PATCH] winedmo/wmvdecod: Expose MP42 video decoding.
Cooper's 9 (Konami, 2006) plays its movies (AVI, Microsoft MPEG-4 V2
video, MP42) through DirectShow into its own RGB24 texture renderer.
The AVI splitter exposes the MP42 stream, but quartz finds no decoder
for it: the AVI Decompressor has no MP42 VfW codec and no DMO lists the
subtype, so the video pin stays unconnected and the movies never show
(the game draws its movie quad white while the audio plays).
As for MP43 (0066), the FFmpeg backend already maps MP42 to
AV_CODEC_ID_MSMPEG4V2 through its RIFF tag table and the decoder is
enabled in the build; only the Windows-side registration and the
winedmo input-type allowlist leave it out.
Advertise MP42 and its lowercase spelling in the WMVideo MFT/DMO
bridge, next to MP43, and accept them in its shared decoder. The
subtype is kept, so FFmpeg selects Microsoft MPEG-4 V2. Extend the MP43
MF regression test to both MP42 spellings.
Tested with Cooper's 9: its MP42 movies play through the WMVideo
Decoder DMO in RGB24 (its WMV9 movies already did).
---
--- a/dlls/mf/tests/transform.c
+++ b/dlls/mf/tests/transform.c
@@ -61,6 +61,8 @@
DEFINE_MEDIATYPE_GUID(MFVideoFormat_P208,MAKEFOURCC('P','2','0','8'));
DEFINE_MEDIATYPE_GUID(MFVideoFormat_VC1S,MAKEFOURCC('V','C','1','S'));
DEFINE_MEDIATYPE_GUID(MFVideoFormat_mp43,MAKEFOURCC('m','p','4','3'));
+DEFINE_MEDIATYPE_GUID(MFVideoFormat_MP42,MAKEFOURCC('M','P','4','2'));
+DEFINE_MEDIATYPE_GUID(MFVideoFormat_mp42,MAKEFOURCC('m','p','4','2'));
DEFINE_GUID(mft_output_sample_incomplete,0xffffff,0xffff,0xffff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff);
@@ -6396,7 +6398,7 @@
static void test_mp43_decoder(void)
{
- const GUID *subtypes[] = {&MFVideoFormat_MP43, &MFVideoFormat_mp43};
+ const GUID *subtypes[] = {&MFVideoFormat_MP43, &MFVideoFormat_mp43, &MFVideoFormat_MP42, &MFVideoFormat_mp42};
const struct attribute_desc input_desc[] =
{
ATTR_GUID(MF_MT_MAJOR_TYPE, MFMediaType_Video),
@@ -6447,7 +6449,7 @@
hr = MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER, MFT_ENUM_FLAG_SYNCMFT,
&input_filter, &output_filter, &activates, &count);
ok(hr == S_OK, "MFTEnumEx returned %#lx.\n", hr);
- ok(count != 0, "No MP43 video decoder found.\n");
+ ok(count != 0, "No MS MPEG-4 video decoder found.\n");
if (SUCCEEDED(hr) && count)
{
hr = IMFActivate_ActivateObject(activates[0], &IID_IMFTransform, (void **)&transform);
--- a/dlls/winedmo/video_decoder.c
+++ b/dlls/winedmo/video_decoder.c
@@ -2521,6 +2521,10 @@
extern const GUID MEDIASUBTYPE_WMV_Unknown;
static const GUID MFVideoFormat_mp43 = {MAKEFOURCC('m','p','4','3'), 0x0000, 0x0010,
{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
+static const GUID MFVideoFormat_MP42 = {MAKEFOURCC('M','P','4','2'), 0x0000, 0x0010,
+ {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
+static const GUID MFVideoFormat_mp42 = {MAKEFOURCC('m','p','4','2'), 0x0000, 0x0010,
+ {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
static const GUID *const wmv_decoder_input_types[] =
{
&MEDIASUBTYPE_WMV1,
@@ -2537,6 +2541,8 @@
&MFVideoFormat_VP90,
&MFVideoFormat_MP43,
&MFVideoFormat_mp43,
+ &MFVideoFormat_MP42,
+ &MFVideoFormat_mp42,
};
static const GUID *const wmv_decoder_output_types[] =
{
--- a/dlls/wmvdecod/wmvdecod.c
+++ b/dlls/wmvdecod/wmvdecod.c
@@ -37,6 +37,8 @@
DEFINE_GUID(DMOVideoFormat_RGB555,D3DFMT_X1R5G5B5,0x524f,0x11ce,0x9f,0x53,0x00,0x20,0xaf,0x0b,0xa7,0x70);
DEFINE_GUID(DMOVideoFormat_RGB8,D3DFMT_P8,0x524f,0x11ce,0x9f,0x53,0x00,0x20,0xaf,0x0b,0xa7,0x70);
DEFINE_MEDIATYPE_GUID(MFVideoFormat_mp43,MAKEFOURCC('m','p','4','3'));
+DEFINE_MEDIATYPE_GUID(MFVideoFormat_MP42,MAKEFOURCC('M','P','4','2'));
+DEFINE_MEDIATYPE_GUID(MFVideoFormat_mp42,MAKEFOURCC('m','p','4','2'));
/***********************************************************************
* DllGetClassObject (wmvdecod.@)
@@ -72,6 +74,8 @@
{MFMediaType_Video, MFVideoFormat_VP90},
{MFMediaType_Video, MFVideoFormat_MP43},
{MFMediaType_Video, MFVideoFormat_mp43},
+ {MFMediaType_Video, MFVideoFormat_MP42},
+ {MFMediaType_Video, MFVideoFormat_mp42},
};
MFT_REGISTER_TYPE_INFO wmv_decoder_mft_outputs[] =
{
@@ -116,6 +120,8 @@
{.type = MEDIATYPE_Video, .subtype = MFVideoFormat_VP90},
{.type = MEDIATYPE_Video, .subtype = MFVideoFormat_MP43},
{.type = MEDIATYPE_Video, .subtype = MFVideoFormat_mp43},
+ {.type = MEDIATYPE_Video, .subtype = MFVideoFormat_MP42},
+ {.type = MEDIATYPE_Video, .subtype = MFVideoFormat_mp42},
};
HRESULT hr;
@@ -0,0 +1,57 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Sun, 25 Oct 2026 16:30:00 +0100
Subject: [PATCH] winedmo: Let the WM reader's seek read the file while
flushing.
reinit_stream() sets read_flushing so that the demux thread's reads
return no data, then seeks the demuxer to the new start time. The seek
reads the file too, through the same callback, which returned no data
either: FFmpeg searches an ASF file without index for its key frames
(asf_read_pts), found nothing and the seek failed ("Failed to seek
demuxer, error -1"). The demuxer then stayed at its end, every following
read failed ("A non-NULL packet sent after an EOF") and the movie stopped
(Yu-Gi-Oh! 5D's Duel Terminal 6 plays its mode previews as segments of
one WMV file, IMediaSeeking::SetPositions() for each).
demuxer_cs already keeps the demux thread out while the demuxer seeks:
let the reads through for the seek's duration.
---
diff --git a/dlls/winedmo/wm_reader.c b/dlls/winedmo/wm_reader.c
--- a/dlls/winedmo/wm_reader.c
+++ b/dlls/winedmo/wm_reader.c
@@ -107,6 +107,7 @@
HANDLE demux_thread;
bool demux_shutdown;
bool read_flushing;
+ bool read_seeking; /* reinit_stream's demuxer seek reads the file while flushing */
unsigned int read_flush_generation;
bool flushing;
CRITICAL_SECTION demuxer_cs; /* serialises winedmo_demuxer_read vs winedmo_demuxer_seek */
@@ -956,7 +957,7 @@
ULONG ret_size = 0;
HRESULT hr;
- if (reader->read_flushing || reader->demux_shutdown)
+ if ((reader->read_flushing && !reader->read_seeking) || reader->demux_shutdown)
{
*size = 0;
return STATUS_SUCCESS;
@@ -2389,9 +2390,13 @@
wm_flush_stream_queue(&reader->streams[i]);
/* Seek demuxer back to start_time. Hold demuxer_cs so the demux thread
- * cannot race us with a concurrent winedmo_demuxer_read call. */
+ * cannot race us with a concurrent winedmo_demuxer_read call. The seek
+ * itself reads the file (an unindexed ASF file is searched for its key
+ * frames), let those reads through. */
EnterCriticalSection(&reader->demuxer_cs);
+ reader->read_seeking = true;
winedmo_demuxer_seek(reader->demuxer, reader->start_time);
+ reader->read_seeking = false;
LeaveCriticalSection(&reader->demuxer_cs);
/* Recreate decoders and reset EOS flags */
--
2.51.0
@@ -0,0 +1,72 @@
From: l12436 <[email protected]>
Subject: [PATCH] winedmo, mfmediaengine: Report RTSP streams as live and don't
signal waiting when playing them.
AVPro Video (VRChat's TopazPlayer) treats MF_MEDIA_ENGINE_EVENT_WAITING as
PlaybackStalled. media_engine_Play() always sends it, so for a live stream
that isn't in the loading network state AVPro keeps reloading the stream.
Report MFMEDIASOURCE_IS_LIVE for rtsp/rtspt/rtspu sources without a duration,
and skip the WAITING event in Play() for live sources.
This replaces the earlier workaround that kept live sources in the loading
network state.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/896
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/892
Tested by the author with a TopazPlayer RTSP stream in VRChat.
---
--- a/dlls/mfmediaengine/main.c
+++ b/dlls/mfmediaengine/main.c
@@ -2803,6 +2803,8 @@
{
struct media_engine *engine = impl_from_IMFMediaEngineEx(iface);
HRESULT hr = S_OK;
+ DWORD characteristics = 0;
+ BOOL is_live = FALSE;
EnterCriticalSection(&engine->cs);
WARN("%p enter flags %#x ready %u network %u.\n", iface, engine->flags, engine->ready_state,
@@ -2835,7 +2837,12 @@
else if (engine->flags & FLAGS_ENGINE_PAUSE_PENDING)
media_engine_set_flag(engine, FLAGS_ENGINE_PLAY_PENDING, TRUE);
- media_engine_notify_live_event(engine, MF_MEDIA_ENGINE_EVENT_WAITING, 0, 0);
+ if (engine->presentation.source && SUCCEEDED(IMFMediaSource_GetCharacteristics(engine->presentation.source, &characteristics)))
+ {
+ is_live = !!(characteristics & MFMEDIASOURCE_IS_LIVE);
+ }
+ if (!is_live)
+ media_engine_notify_live_event(engine, MF_MEDIA_ENGINE_EVENT_WAITING, 0, 0);
}
WARN("%p leave hr %#lx flags %#x ready %u network %u.\n", iface, hr, engine->flags,
--- a/dlls/winedmo/media_source.c
+++ b/dlls/winedmo/media_source.c
@@ -350,6 +350,7 @@
UINT64 duration;
bool has_duration;
bool is_network;
+ bool is_rtsp;
struct winedmo_demuxer demuxer;
struct
@@ -1986,6 +1987,7 @@
{
*characteristics = MFMEDIASOURCE_CAN_PAUSE;
if (source->has_duration) *characteristics |= MFMEDIASOURCE_CAN_SEEK;
+ if (source->is_rtsp && !source->has_duration) *characteristics |= MFMEDIASOURCE_IS_LIVE;
}
LeaveCriticalSection(&source->cs);
@@ -2247,6 +2249,10 @@
|| !wcsnicmp(context->url, L"rtsp://", 7)
|| !wcsnicmp(context->url, L"rtspt://", 8)
|| !wcsnicmp(context->url, L"rtspu://", 8));
+ object->is_rtsp = context->url &&
+ (!wcsnicmp(context->url, L"rtsp://", 7)
+ || !wcsnicmp(context->url, L"rtspt://", 8)
+ || !wcsnicmp(context->url, L"rtspu://", 8));
object->rate = 1.0f;
InitializeCriticalSectionEx(&object->cs, 0, RTL_CRITICAL_SECTION_FLAG_FORCE_DEBUG_INFO);
object->cs.DebugInfo->Spare[0] = (DWORD_PTR)(__FILE__ ": cs");
@@ -0,0 +1,64 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Mon, 26 Oct 2026 23:30:00 +0100
Subject: [PATCH] mfreadwrite: Find the WMV decoder for MF RGB output types.
The WMV video decoder registers its RGB outputs with the DMO subtypes
(DMOVideoFormat_RGB32...: the D3D format with the DirectShow GUID base),
as native does, not with the MFVideoFormat_RGB* subtypes. A source reader
asked for MFVideoFormat_RGB32 on a WMV3 stream, without the video
processor, enumerated the video decoders for WMV3 -> MFVideoFormat_RGB32,
found none and SetCurrentMediaType() failed with MF_E_TOPO_CODEC_NOT_FOUND
(Wangan Midnight Maximum Tune 5 then stops on its movies; the decoder
itself accepts MFVideoFormat_RGB32 as its output type).
Like the IYUV fixup, enumerate again with the DMO subtype when no decoder
matches an MF RGB output subtype.
---
--- a/dlls/mfreadwrite/reader.c
+++ b/dlls/mfreadwrite/reader.c
@@ -3033,6 +3033,24 @@
return hr;
}
+/* The DMO RGB subtypes (DMOVideoFormat_RGB32...) keep the D3D format of the MF subtype with the DirectShow GUID base. */
+static BOOL get_dmo_rgb_subtype(const GUID *category, MFT_REGISTER_TYPE_INFO *type)
+{
+ static const GUID dmo_base = {0, 0x524f, 0x11ce, {0x9f, 0x53, 0x00, 0x20, 0xaf, 0x0b, 0xa7, 0x70}};
+
+ if (!IsEqualGUID(category, &MFT_CATEGORY_VIDEO_DECODER) || !IsEqualGUID(&type->guidMajorType, &MFMediaType_Video))
+ return FALSE;
+ if (!IsEqualGUID(&type->guidSubtype, &MFVideoFormat_RGB32) && !IsEqualGUID(&type->guidSubtype, &MFVideoFormat_RGB24)
+ && !IsEqualGUID(&type->guidSubtype, &MFVideoFormat_RGB565) && !IsEqualGUID(&type->guidSubtype, &MFVideoFormat_RGB555)
+ && !IsEqualGUID(&type->guidSubtype, &MFVideoFormat_RGB8))
+ return FALSE;
+
+ type->guidSubtype.Data2 = dmo_base.Data2;
+ type->guidSubtype.Data3 = dmo_base.Data3;
+ memcpy(type->guidSubtype.Data4, dmo_base.Data4, sizeof(dmo_base.Data4));
+ return TRUE;
+}
+
static HRESULT source_reader_create_transform(struct source_reader *reader, BOOL decoder, BOOL allow_processor,
IMFMediaType *input_type, IMFMediaType *output_type, struct transform_entry **out)
{
@@ -3102,7 +3120,18 @@
}
count = 0;
- if (SUCCEEDED(hr = MFTEnumEx(category, 0, &in_type, allow_processor ? NULL : &out_type, &activates, &count)))
+ hr = MFTEnumEx(category, 0, &in_type, allow_processor ? NULL : &out_type, &activates, &count);
+ if (SUCCEEDED(hr) && !count && !allow_processor && get_dmo_rgb_subtype(&category, &out_type))
+ {
+ /* The WMV video decoder registers its RGB outputs with the DMO subtypes (as native does), but selecting
+ * the matching MF RGB format as an output format still succeeds, so fixup the subtype for MFTEnumEx.
+ */
+ WARN("Fixing up %s subtype for the video decoder\n", debugstr_guid(&out_type.guidSubtype));
+ CoTaskMemFree(activates);
+ count = 0;
+ hr = MFTEnumEx(category, 0, &in_type, &out_type, &activates, &count);
+ }
+ if (SUCCEEDED(hr))
{
if (!count)
{
@@ -0,0 +1,138 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Tue, 27 Oct 2026 00:40:00 +0100
Subject: [PATCH] quartz/vmr9: Convert NV12 and YV12 samples rendered to an
RGB32 surface.
A custom surface allocator can create the VMR-9's surfaces in another
format than the stream's: Need for Speed Underground's arcade frontend
(Global VR, UniverShell2.exe) asks for an X8R8G8B8 texture surface
(AllocateSurfaceHelper with VMR9AllocFlag_TextureSurface) while its WMV
movies reach the renderer as NV12. vmr_render() then copied the NV12
planes as they are: the luma plane fills the RGB32 surface (640 bytes
per 2560-byte row), copy_plane() moves the destination to the end of the
surface, and the chroma plane is written past it. The process crashed in
memcpy() from vmr_render(), at its first frame (attract movie).
When the surface is X8R8G8B8 or A8R8G8B8 and the sample NV12 or YV12,
convert the sample with the converters the VMR-7 renderer already uses
for this case, now shared.
GE: also set the alpha channel in the converters, so an A8R8G8B8 surface that
a presenter blends is opaque. VMR-7 only uses them for X8R8G8B8 surfaces.
GE: the converters also take the source height separately, which locates
the chroma planes, and convert at most the surface size: a surface smaller
than the frame no longer falls back to the plane copy in VMR-9, and VMR-7
no longer writes past a smaller RGB32 surface either.
---
--- a/dlls/quartz/quartz_private.h
+++ b/dlls/quartz/quartz_private.h
@@ -70,6 +70,11 @@
HRESULT video_renderer_default_create(IUnknown *outer, IUnknown **out);
HRESULT vmr7_presenter_create(IUnknown *outer, IUnknown **out);
HRESULT vmr7_create(IUnknown *outer, IUnknown **out);
+
+void convert_nv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
+ const BYTE *src, unsigned int src_pitch, unsigned int src_height, unsigned int width, unsigned int height);
+void convert_yv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
+ const BYTE *src, unsigned int src_pitch, unsigned int src_height, unsigned int width, unsigned int height);
HRESULT vmr9_create(IUnknown *outer, IUnknown **out);
HRESULT wave_parser_create(IUnknown *outer, IUnknown **out);
--- a/dlls/quartz/vmr7.c
+++ b/dlls/quartz/vmr7.c
@@ -170,11 +170,13 @@
return value;
}
-static void convert_nv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
- const BYTE *src, unsigned int src_pitch, unsigned int width, unsigned int height)
+/* src_height locates the chroma planes; width and height are the size to convert,
+ * at most the source size, so that callers can clip to a smaller surface. */
+void convert_nv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
+ const BYTE *src, unsigned int src_pitch, unsigned int src_height, unsigned int width, unsigned int height)
{
const BYTE *y_plane = src;
- const BYTE *uv_plane = src + src_pitch * height;
+ const BYTE *uv_plane = src + src_pitch * src_height;
unsigned int x, y;
for (y = 0; y < height; ++y)
@@ -197,17 +199,17 @@
g = clamp_rgb_value((298 * c - 100 * d - 208 * e + 128) >> 8);
b = clamp_rgb_value((298 * c + 516 * d + 128) >> 8);
- dst_row[x] = (r << 16) | (g << 8) | b;
+ dst_row[x] = 0xff000000 | (r << 16) | (g << 8) | b;
}
}
}
-static void convert_yv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
- const BYTE *src, unsigned int src_pitch, unsigned int width, unsigned int height)
+void convert_yv12_to_rgb32(BYTE *dst, unsigned int dst_pitch,
+ const BYTE *src, unsigned int src_pitch, unsigned int src_height, unsigned int width, unsigned int height)
{
const BYTE *y_plane = src;
- const BYTE *v_plane = src + src_pitch * height;
- const BYTE *u_plane = v_plane + (src_pitch / 2) * (height / 2);
+ const BYTE *v_plane = src + src_pitch * src_height;
+ const BYTE *u_plane = v_plane + (src_pitch / 2) * (src_height / 2);
unsigned int x, y;
for (y = 0; y < height; ++y)
@@ -231,7 +233,7 @@
g = clamp_rgb_value((298 * c - 100 * d - 208 * e + 128) >> 8);
b = clamp_rgb_value((298 * c + 516 * d + 128) >> 8);
- dst_row[x] = (r << 16) | (g << 8) | b;
+ dst_row[x] = 0xff000000 | (r << 16) | (g << 8) | b;
}
}
}
@@ -339,7 +341,8 @@
&& (surface_desc.ddpfPixelFormat.dwFlags & DDPF_RGB)
&& surface_desc.ddpfPixelFormat.dwRGBBitCount == 32)
{
- convert_nv12_to_rgb32(surface_desc.lpSurface, surface_desc.lPitch, data, src_pitch, width, abs(height));
+ convert_nv12_to_rgb32(surface_desc.lpSurface, surface_desc.lPitch, data, src_pitch, abs(height),
+ min(width, surface_desc.dwWidth), min(abs(height), surface_desc.dwHeight));
}
else if (bitmap_header->biCompression == mmioFOURCC('N','V','1','2'))
{
@@ -357,7 +360,8 @@
if ((surface_desc.ddpfPixelFormat.dwFlags & DDPF_RGB)
&& surface_desc.ddpfPixelFormat.dwRGBBitCount == 32)
{
- convert_yv12_to_rgb32(surface_desc.lpSurface, surface_desc.lPitch, data, src_pitch, width, abs(height));
+ convert_yv12_to_rgb32(surface_desc.lpSurface, surface_desc.lPitch, data, src_pitch, abs(height),
+ min(width, surface_desc.dwWidth), min(abs(height), surface_desc.dwHeight));
}
else
{
--- a/dlls/quartz/vmr9.c
+++ b/dlls/quartz/vmr9.c
@@ -293,7 +293,21 @@
return hr;
}
- if (bitmap_header->biCompression == mmioFOURCC('N','V','1','2'))
+ if ((bitmap_header->biCompression == mmioFOURCC('N','V','1','2')
+ || bitmap_header->biCompression == mmioFOURCC('Y','V','1','2'))
+ && (dst_desc.Format == D3DFMT_X8R8G8B8 || dst_desc.Format == D3DFMT_A8R8G8B8))
+ {
+ /* A custom allocator can give an RGB32 surface for a YUV stream: copying the planes
+ * there would write the chroma past the end of the surface, convert them instead,
+ * clipped to the surface. */
+ unsigned int rgb_width = min(width, dst_desc.Width), rgb_height = min(abs(height), dst_desc.Height);
+
+ if (bitmap_header->biCompression == mmioFOURCC('N','V','1','2'))
+ convert_nv12_to_rgb32(locked_rect.pBits, locked_rect.Pitch, data, src_pitch, abs(height), rgb_width, rgb_height);
+ else
+ convert_yv12_to_rgb32(locked_rect.pBits, locked_rect.Pitch, data, src_pitch, abs(height), rgb_width, rgb_height);
+ }
+ else if (bitmap_header->biCompression == mmioFOURCC('N','V','1','2'))
{
BYTE *dst = locked_rect.pBits;
const BYTE *src = data;
@@ -0,0 +1,101 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Thu, 30 Oct 2026 19:10:00 +0100
Subject: [PATCH] winedmo: Keep the WM reader's flush from freeing a sample
being read.
reader_GetNextSample() for a given stream number leaves reader->cs
before wm_reader_read_stream_sample(), which copies the stream's
partially consumed packet (current_data) into the output sample. A seek
(reinit_stream()) or reader_Close() on another thread flushes the
stream queues meanwhile, and wm_flush_stream_queue() freed current_data
with no lock: the copy read freed memory and crashed in memcpy (the
async reader's read thread, wmvcore). EA SPORTS Madden NFL Football
Season 2's (Global VR) cabinet frontend crashed at the end of almost
every attract movie.
Serialize the consumer's read with the flush of current_data with a
per-stream lock. Both flush callers set queue_flushing first, so a
consumer waiting for a packet with the lock held returns.
---
diff --git a/dlls/winedmo/wm_reader.c b/dlls/winedmo/wm_reader.c
--- a/dlls/winedmo/wm_reader.c
+++ b/dlls/winedmo/wm_reader.c
@@ -59,7 +59,9 @@
bool queue_eos;
bool queue_flushing;
- /* Partial packet consumption (for audio chunking) */
+ /* Partial packet consumption (for audio chunking), protected by read_cs:
+ * a consumer copies from current_data without holding reader->cs. */
+ CRITICAL_SECTION read_cs;
BYTE *current_data;
UINT32 current_size;
UINT32 current_offset;
@@ -1015,9 +1017,13 @@
LeaveCriticalSection(&stream->queue_cs);
WakeAllConditionVariable(&stream->queue_cv);
+ /* A consumer may be copying from current_data: queue_flushing is set by
+ * the callers, a consumer waiting for a packet returns without it. */
+ EnterCriticalSection(&stream->read_cs);
free(stream->current_data);
stream->current_data = NULL;
stream->current_size = stream->current_offset = 0;
+ LeaveCriticalSection(&stream->read_cs);
}
static BOOL wm_stream_wait_queue_space(struct wm_reader *reader, struct wm_stream *stream)
@@ -2311,6 +2317,7 @@
stream->index = i;
list_init(&stream->packet_queue);
InitializeCriticalSection(&stream->queue_cs);
+ InitializeCriticalSection(&stream->read_cs);
InitializeConditionVariable(&stream->queue_cv);
InitializeCriticalSection(&stream->decoder_cs);
wm_stream_reset_decoded_timing(stream);
@@ -2356,6 +2363,7 @@
free(reader->streams[i].compressed_fmt);
DeleteCriticalSection(&reader->streams[i].decoder_cs);
DeleteCriticalSection(&reader->streams[i].queue_cs);
+ DeleteCriticalSection(&reader->streams[i].read_cs);
}
free(reader->streams);
reader->streams = NULL;
@@ -2469,7 +2477,7 @@
return S_OK;
}
-static HRESULT wm_reader_read_stream_sample(struct wm_reader *reader, struct wm_stream *stream,
+static HRESULT wm_reader_read_stream_sample_locked(struct wm_reader *reader, struct wm_stream *stream,
INSSBuffer **sample, QWORD *pts, QWORD *duration, DWORD *flags)
{
BYTE *raw_data = NULL;
@@ -2544,6 +2552,17 @@
return S_OK;
}
+static HRESULT wm_reader_read_stream_sample(struct wm_reader *reader, struct wm_stream *stream,
+ INSSBuffer **sample, QWORD *pts, QWORD *duration, DWORD *flags)
+{
+ HRESULT hr;
+
+ EnterCriticalSection(&stream->read_cs);
+ hr = wm_reader_read_stream_sample_locked(reader, stream, sample, pts, duration, flags);
+ LeaveCriticalSection(&stream->read_cs);
+ return hr;
+}
+
static BOOL wm_stream_peek_pending_sample(struct wm_stream *stream, INT64 *pts)
{
BOOL has_sample = FALSE;
@@ -2768,6 +2787,7 @@
wm_flush_stream_queue(stream);
DeleteCriticalSection(&stream->decoder_cs);
DeleteCriticalSection(&stream->queue_cs);
+ DeleteCriticalSection(&stream->read_cs);
free(stream->compressed_fmt);
stream->compressed_fmt = NULL;
if (stream->output_allocator)
--
2.51.0
@@ -0,0 +1,114 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Sun, 1 Nov 2026 10:30:00 +0100
Subject: [PATCH] winedmo: Detect backward video input from the previous input
timestamp.
The video decoder flushes its transform when an input timestamp is more
than 0.2 s before decoder->sample_time, taken as a seek back. But
sample_time is the last output's pts plus its duration, and for WMV the
unix side makes a frame last as long as the largest input delta seen
(up to 2 s): after a gap in a stream's timestamps (frames the encoder
dropped on a still picture), every following frame lasts that long, the
next input on time looks 1.5 s back and the decoder is flushed. The
next P-frame then decodes against no reference, gray blocks until the
next key frame.
The attract movies of PGA TOUR Golf Championship Edition III (Global
VR, WMV3 played through quartz's DMO wrapper) were corrupted after each
such gap, from a fade until the next key frame.
Compare with the previous input timestamp instead, in the DMO and MFT
paths.
GE: keep the existing sample_time condition as well and only reset when the
input is behind both: this decoder also handles H.264 through MF, where input
timestamps arrive in decode order and can go back from the previous input on
B-frames, so the check never resets more often than before.
---
--- a/dlls/winedmo/video_decoder.c
+++ b/dlls/winedmo/video_decoder.c
@@ -123,6 +123,8 @@
const GUID *const *default_output_types;
UINT64 sample_time;
+ /* the last input timestamp, INT64_MIN when none */
+ LONGLONG last_input_time;
IMFMediaType *input_type;
MFT_INPUT_STREAM_INFO input_info;
IMFMediaType *output_type;
@@ -1162,12 +1164,14 @@
case MFT_MESSAGE_COMMAND_FLUSH:
if (!decoder->winedmo_transform.handle) return MF_E_TRANSFORM_TYPE_NOT_SET;
decoder->sample_time = -1;
+ decoder->last_input_time = INT64_MIN;
decoder->last_output_pts = INT64_MIN;
decoder->observed_output_duration = 0;
return winedmo_transform_flush(decoder->winedmo_transform) ? E_FAIL : S_OK;
case MFT_MESSAGE_NOTIFY_START_OF_STREAM:
decoder->sample_time = -1;
+ decoder->last_input_time = INT64_MIN;
decoder->last_output_pts = INT64_MIN;
decoder->observed_output_duration = 0;
return S_OK;
@@ -1220,15 +1224,21 @@
if (SUCCEEDED(IMFSample_GetUINT32(sample, &MFSampleExtension_Discontinuity, &value)) && value)
push_flags |= WINEDMO_SAMPLE_FLAG_DISCONTINUITY;
- if (time != INT64_MIN && decoder->sample_time != -1 && time + 2000000 < decoder->sample_time)
+ /* Also compared with the previous input: the output time (pts + duration) runs ahead after a
+ * gap in the stream's timestamps (a WMV frame lasting up to the largest input delta seen),
+ * while H.264 input in decode order can go back from the previous input on B-frames. */
+ if (time != INT64_MIN && decoder->sample_time != -1 && time + 2000000 < decoder->sample_time
+ && decoder->last_input_time != INT64_MIN && time + 2000000 < decoder->last_input_time)
{
TRACE("Resetting decoder timeline on backward input timestamp %s -> %s.\n",
- wine_dbgstr_longlong(decoder->sample_time), wine_dbgstr_longlong(time));
+ wine_dbgstr_longlong(decoder->last_input_time), wine_dbgstr_longlong(time));
decoder->sample_time = -1;
winedmo_transform_flush(decoder->winedmo_transform);
push_flags |= WINEDMO_SAMPLE_FLAG_DISCONTINUITY;
}
+ if (time != INT64_MIN)
+ decoder->last_input_time = time;
status = winedmo_transform_push_input(decoder->winedmo_transform, data, size, time, dts, duration, push_flags);
IMFMediaBuffer_Unlock(buffer);
IMFMediaBuffer_Release(buffer);
@@ -2110,6 +2120,7 @@
return DMO_E_TYPE_NOT_SET;
decoder->sample_time = -1;
+ decoder->last_input_time = INT64_MIN;
if (winedmo_transform_flush(decoder->winedmo_transform))
return E_FAIL;
@@ -2175,14 +2186,17 @@
if (flags & DMO_INPUT_DATA_BUFFERF_DISCONTINUITY) push_flags |= WINEDMO_SAMPLE_FLAG_DISCONTINUITY;
if ((flags & DMO_INPUT_DATA_BUFFERF_TIME) && decoder->sample_time != -1
- && timestamp + 2000000 < decoder->sample_time)
+ && timestamp + 2000000 < decoder->sample_time
+ && decoder->last_input_time != INT64_MIN && timestamp + 2000000 < decoder->last_input_time)
{
TRACE("Resetting decoder timeline on backward DMO input timestamp %s -> %s.\n",
- wine_dbgstr_longlong(decoder->sample_time), wine_dbgstr_longlong(timestamp));
+ wine_dbgstr_longlong(decoder->last_input_time), wine_dbgstr_longlong(timestamp));
decoder->sample_time = -1;
winedmo_transform_flush(decoder->winedmo_transform);
push_flags |= WINEDMO_SAMPLE_FLAG_DISCONTINUITY;
}
+ if (flags & DMO_INPUT_DATA_BUFFERF_TIME)
+ decoder->last_input_time = timestamp;
status = winedmo_transform_push_input(decoder->winedmo_transform, data, size,
(flags & DMO_INPUT_DATA_BUFFERF_TIME) ? timestamp : INT64_MIN,
@@ -2416,6 +2430,7 @@
decoder->default_output_type_count = output_type_count;
decoder->default_output_types = output_types;
decoder->last_output_pts = INT64_MIN;
+ decoder->last_input_time = INT64_MIN;
if (FAILED(hr = MFCreateMediaType(&decoder->stream_type)))
goto failed;
@@ -0,0 +1,66 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexandre Derumier <[email protected]>
Date: Mon, 2 Nov 2026 14:00:00 +0100
Subject: [PATCH] winedmo: Rewind the DirectShow parser after it delivered
samples.
dmo_seeking_SetPositions() treats an absolute position equal to
seek.llCurrent as unchanged and only records it, without flushing or
seeking. But seek.llCurrent is the position the streams were last sought
to, not where they are: once a stream thread delivered samples or
EndOfStream, seeking back to that position must seek.
Frenzy Express (Uniana's 2001 arcade scooter game, a Windows PC
based cabinet) loops its music (MP3 through the MPEG-I splitter) with
IMediaSeeking::SetPositions(0) on the filter graph when it gets
EC_COMPLETE. The parser skipped the seek since its position was still 0:
it never delivered anything again, nor EndOfStream (already sent), and
the graph's next state change waited forever for the DirectSound
renderer, hanging the game's window thread.
Treat the position as unchanged only while no active pin delivered a
sample or EndOfStream since the last seek or start.
---
dlls/winedmo/winedmo_quartz_parser.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/dlls/winedmo/winedmo_quartz_parser.c b/dlls/winedmo/winedmo_quartz_parser.c
--- a/dlls/winedmo/winedmo_quartz_parser.c
+++ b/dlls/winedmo/winedmo_quartz_parser.c
@@ -353,6 +353,23 @@
return pin->selected && pin->pin.pin.peer;
}
+/* Whether a stream thread delivered a sample or EndOfStream since the last seek or start: the
+ * streams moved away from seek.llCurrent, the position they were sought to. */
+static BOOL dmo_parser_delivered_since_seek(const struct dmo_parser *filter)
+{
+ unsigned int i;
+
+ for (i = 0; i < filter->source_count; ++i)
+ {
+ const struct dmo_parser_source *pin = filter->sources[i];
+
+ if (dmo_parser_source_is_active(pin) && (!pin->need_segment || pin->eos_sent))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
static BOOL dmo_parser_has_selected_stream_type(const struct dmo_parser *filter, const GUID *major_type)
{
unsigned int i;
@@ -619,7 +636,8 @@
}
current_unchanged = !current || current_positioning == AM_SEEKING_NoPositioning
- || (current_positioning == AM_SEEKING_AbsolutePositioning && *current == pin->seek.llCurrent)
+ || (current_positioning == AM_SEEKING_AbsolutePositioning && *current == pin->seek.llCurrent
+ && !dmo_parser_delivered_since_seek(filter))
|| (current_positioning == AM_SEEKING_RelativePositioning && !*current)
|| (current_positioning == AM_SEEKING_IncrementalPositioning && !*current);
if (current_unchanged)
--
2.51.0
@@ -0,0 +1,63 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GE-Proton <git@localhost>
Subject: [PATCH] lsteamclient: Add Armored Core VI Steam Input actions.
Armored Core VI: Fires of Rubicon (1888160) reads controllers only through
ISteamInput006 and never polls XInput itself. When the XInput-backed Steam
Input fallback serves the game, none of its action names were known, so every
action stayed inactive and the game received no controller input. On current
builds this is the default path for a DualShock 4 / DualSense / DualSense Edge
when Steam Input is disabled (automatic Sony fallback).
Add the game's single action set and its 18 actions from the layouts shipped
with its Game/steam_input_manifest.vdf. The Xbox One and PlayStation layouts use
the same action set and one-to-one physical bindings, except that the
PlayStation layouts bind pad_c_left to the touchpad click; XInput has no
touchpad button, so under the fallback pad_c_left comes from Back (Create/Share).
---
diff --git a/lsteamclient/steam_input_manual.c b/lsteamclient/steam_input_manual.c
--- a/lsteamclient/steam_input_manual.c
+++ b/lsteamclient/steam_input_manual.c
@@ -148,6 +148,22 @@
DIGITAL_BUTTON("MenuLeft", XINPUT_GAMEPAD_DPAD_LEFT),
DIGITAL_BUTTON("MenuCharacter", XINPUT_GAMEPAD_BACK),
DIGITAL_BUTTON("MenuExit", XINPUT_GAMEPAD_START),
+
+ /* Armored Core VI: Fires of Rubicon. */
+ DIGITAL_BUTTON("pad_l_up", XINPUT_GAMEPAD_DPAD_UP),
+ DIGITAL_BUTTON("pad_l_down", XINPUT_GAMEPAD_DPAD_DOWN),
+ DIGITAL_BUTTON("pad_l_left", XINPUT_GAMEPAD_DPAD_LEFT),
+ DIGITAL_BUTTON("pad_l_right", XINPUT_GAMEPAD_DPAD_RIGHT),
+ DIGITAL_BUTTON("pad_r_up", XINPUT_GAMEPAD_Y),
+ DIGITAL_BUTTON("pad_r_down", XINPUT_GAMEPAD_A),
+ DIGITAL_BUTTON("pad_r_left", XINPUT_GAMEPAD_X),
+ DIGITAL_BUTTON("pad_r_right", XINPUT_GAMEPAD_B),
+ DIGITAL_BUTTON("pad_c_left", XINPUT_GAMEPAD_BACK),
+ DIGITAL_BUTTON("pad_c_right", XINPUT_GAMEPAD_START),
+ DIGITAL_BUTTON("pad_l_shoulder", XINPUT_GAMEPAD_LEFT_SHOULDER),
+ DIGITAL_BUTTON("pad_r_shoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER),
+ DIGITAL_BUTTON("pad_l_thumb", XINPUT_GAMEPAD_LEFT_THUMB),
+ DIGITAL_BUTTON("pad_r_thumb", XINPUT_GAMEPAD_RIGHT_THUMB),
};
static struct xinput_analog_action xinput_analog_actions[] =
@@ -163,6 +179,11 @@
{"right_stick", 0, XINPUT_ANALOG_RIGHT_STICK},
{"Move", 0, XINPUT_ANALOG_LEFT_STICK},
{"Camera", 0, XINPUT_ANALOG_RIGHT_STICK},
+ /* Armored Core VI: Fires of Rubicon. */
+ {"left_analog_controls", 0, XINPUT_ANALOG_LEFT_STICK},
+ {"right_analog_controls", 0, XINPUT_ANALOG_RIGHT_STICK},
+ {"pad_l_trigger", 0, XINPUT_ANALOG_LEFT_TRIGGER},
+ {"pad_r_trigger", 0, XINPUT_ANALOG_RIGHT_TRIGGER},
};
static struct
@@ -174,6 +195,7 @@
{"GamepadSetting", 0},
{"GameControls", 0},
{"MenuControls", 0},
+ {"fnr_controls", 0},
};
static BOOL env_enabled(const char *name)
@@ -0,0 +1,197 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Thomas Belway <[email protected]>
Date: Thu, 24 Sep 2026 16:33:50 -0400
Subject: [PATCH] lsteamclient: Ignore repeated ReleaseRequest() on the same
server list request.
The Forest (242760) calls ISteamMatchmakingServers::ReleaseRequest() two or
three times on the same HServerListRequest when joining a dedicated server
from the in-game browser, the last time from a different thread. This is
harmless with the native Windows client, but here the handle is our own
w_request allocation: the second call passes freed memory to the native
client (ReleaseRequest() on a stale u_request, free_callback_obj() on a stale
u_response) and frees the request again, and glibc aborts with "double free
or corruption" or the heap corruption crashes shortly afterwards.
Track live requests and ignore releases of requests that are not live.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
--- a/lsteamclient/steam_matchmaking_manual.c
+++ b/lsteamclient/steam_matchmaking_manual.c
@@ -28,6 +28,51 @@
return NULL;
}
+/* Some games (e.g. The Forest) call ReleaseRequest more than once on the same
+ * server list request handle, sometimes from different threads. This is
+ * harmless on Windows, but here the handle is our own w_request allocation, so
+ * a second release would pass freed memory to the native client and free it
+ * again. Keep track of live requests and ignore releases of unknown ones. */
+struct live_request
+{
+ struct list entry;
+ struct w_request *request;
+};
+
+static struct list live_requests = LIST_INIT( live_requests );
+static CRITICAL_SECTION live_requests_cs = { NULL, -1, 0, 0, 0, 0 };
+
+static void track_request( struct w_request *request )
+{
+ struct live_request *live;
+
+ if (!(live = HeapAlloc( GetProcessHeap(), 0, sizeof(*live) ))) return;
+ live->request = request;
+
+ EnterCriticalSection( &live_requests_cs );
+ list_add_tail( &live_requests, &live->entry );
+ LeaveCriticalSection( &live_requests_cs );
+}
+
+static BOOL untrack_request( struct w_request *request )
+{
+ struct live_request *live;
+ BOOL found = FALSE;
+
+ EnterCriticalSection( &live_requests_cs );
+ LIST_FOR_EACH_ENTRY( live, &live_requests, struct live_request, entry )
+ {
+ if (live->request != request) continue;
+ list_remove( &live->entry );
+ HeapFree( GetProcessHeap(), 0, live );
+ found = TRUE;
+ break;
+ }
+ LeaveCriticalSection( &live_requests_cs );
+
+ return found;
+}
+
void __thiscall winISteamMatchmakingServers_SteamMatchMakingServers001_CancelQuery(struct w_iface *_this, uint32_t eType)
{
struct ISteamMatchmakingServers_SteamMatchMakingServers001_CancelQuery_params params =
@@ -63,6 +108,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -86,6 +132,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -112,6 +159,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -138,6 +186,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -164,6 +213,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -190,6 +240,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -215,6 +266,13 @@
struct w_request *request = get_w_request( hServerListRequest );
TRACE( "%p %p\n", _this, hServerListRequest );
+
+ if (request && !untrack_request( request ))
+ {
+ WARN( "ignoring release of unknown or already released request %p\n", request );
+ return;
+ }
+
execute_pending_callbacks(); /* execute any pending callbacks that might still need to use the request */
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers002_ReleaseRequest, &params );
@@ -288,6 +346,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -312,6 +371,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -338,6 +398,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -364,6 +425,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -390,6 +452,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -416,6 +479,7 @@
return NULL;
}
+ track_request( request );
return request;
}
@@ -441,6 +505,13 @@
struct w_request *request = get_w_request( hServerListRequest );
TRACE( "%p %p\n", _this, hServerListRequest );
+
+ if (request && !untrack_request( request ))
+ {
+ WARN( "ignoring release of unknown or already released request %p\n", request );
+ return;
+ }
+
execute_pending_callbacks(); /* execute any pending callbacks that might still need to use the request */
STEAMCLIENT_CALL( ISteamMatchmakingServers_SteamMatchMakingServers003_ReleaseRequest, &params );
@@ -0,0 +1,402 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] lsteamclient: Optionally report Steam Deck and Steam Controller as
Xbox One controllers.
Some games only have prompts for the gamepad types they know. Rune Factory 5
uses ISteamInput002, asks GetInputTypeForHandle() for the device type and
shows keyboard prompts on a Steam Deck or with a Steam Controller.
With PROTON_STEAM_INPUT_XBOX_PROMPTS=1, report native Steam Controller and
Steam Deck handles as k_ESteamInputType_XBoxOneController, and translate
SteamController/SteamDeck action origins returned by Get*ActionOrigins() to
Xbox One origins through the interface's own TranslateActionOrigin(), so
glyph and string lookups return Xbox prompts. Synthetic XInput fallback
handles keep their existing handling.
Update the wrapper generator and checked-in wrappers together.
https://github.com/GloriousEggroll/proton-ge-custom/issues/841
---
--- a/lsteamclient/gen_wrapper.py
+++ b/lsteamclient/gen_wrapper.py
@@ -1185,6 +1185,11 @@
"ISteamController_SteamController007") else "FALSE"
session_config_method = None
connected_controllers_method = None
+ translate_origin_method = None
+
+ if is_steaminput and method.name in ("GetDigitalActionOrigins", "GetAnalogActionOrigins"):
+ translate_origin_method = next((candidate for candidate in klass.methods
+ if candidate.name == "TranslateActionOrigin"), None)
if (is_steaminput or is_legacy_steaminput) and method.name == "Init":
session_config_method = next((candidate for candidate in klass.methods
@@ -1295,6 +1300,26 @@
out(u' params._ret = steaminput006_xinput_register_digital_action( params._ret, pszActionName );\n')
elif is_steaminput and method.name == "GetAnalogActionHandle":
out(u' params._ret = steaminput006_xinput_register_analog_action( params._ret, pszActionName );\n')
+ elif is_steaminput and method.name == "GetInputTypeForHandle":
+ out(u' params._ret = steaminput_native_input_type( params._ret );\n')
+ elif is_steaminput and method.name in ("GetDigitalActionOrigins", "GetAnalogActionOrigins") and translate_origin_method:
+ out(u' if (steaminput_report_xbox_identity())\n')
+ out(u' {\n')
+ out(f' struct {translate_origin_method.full_name}_params translate_params =\n')
+ out(u' {\n')
+ out(u' .u_iface = _this->u_iface,\n')
+ out(u' .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */\n')
+ out(u' };\n')
+ out(u' int32_t i;\n')
+ out(u'\n')
+ out(u' for (i = 0; i < params._ret; i++)\n')
+ out(u' {\n')
+ out(u' if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;\n')
+ out(u' translate_params.eSourceOrigin = originsOut[i];\n')
+ out(f' STEAMCLIENT_CALL( {translate_origin_method.full_name}, &translate_params );\n')
+ out(u' if (translate_params._ret) originsOut[i] = translate_params._ret;\n')
+ out(u' }\n')
+ out(u' }\n')
if method.name in OUTSTR_PARAMS and OUTSTR_PARAMS[method.name] in names:
out(f' if ({OUTSTR_PARAMS[method.name]}) *{OUTSTR_PARAMS[method.name]} = get_unix_buffer( params._str );\n')
--- a/lsteamclient/winISteamInput.c
+++ b/lsteamclient/winISteamInput.c
@@ -223,6 +223,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput001_GetDigitalActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput001_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput001_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -267,6 +284,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput001_GetAnalogActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput001_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput001_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -388,6 +422,7 @@
TRACE("%p\n", _this);
if (steaminput006_xinput_get_input_type( &params._ret, inputHandle, TRUE )) return params._ret;
STEAMCLIENT_CALL( ISteamInput_SteamInput001_GetInputTypeForHandle, &params );
+ params._ret = steaminput_native_input_type( params._ret );
return params._ret;
}
@@ -755,6 +790,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput002_GetDigitalActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput002_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput002_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -799,6 +851,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput002_GetAnalogActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput002_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput002_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -920,6 +989,7 @@
TRACE("%p\n", _this);
if (steaminput006_xinput_get_input_type( &params._ret, inputHandle, FALSE )) return params._ret;
STEAMCLIENT_CALL( ISteamInput_SteamInput002_GetInputTypeForHandle, &params );
+ params._ret = steaminput_native_input_type( params._ret );
return params._ret;
}
@@ -1364,6 +1434,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput005_GetDigitalActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput005_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput005_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -1420,6 +1507,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput005_GetAnalogActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput005_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput005_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -1586,6 +1690,7 @@
TRACE("%p\n", _this);
if (steaminput006_xinput_get_input_type( &params._ret, inputHandle, FALSE )) return params._ret;
STEAMCLIENT_CALL( ISteamInput_SteamInput005_GetInputTypeForHandle, &params );
+ params._ret = steaminput_native_input_type( params._ret );
return params._ret;
}
@@ -2055,6 +2160,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput006_GetDigitalActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput006_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput006_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -2111,6 +2233,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput006_GetAnalogActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput006_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput006_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -2277,6 +2416,7 @@
TRACE("%p\n", _this);
if (steaminput006_xinput_get_input_type( &params._ret, inputHandle, FALSE )) return params._ret;
STEAMCLIENT_CALL( ISteamInput_SteamInput006_GetInputTypeForHandle, &params );
+ params._ret = steaminput_native_input_type( params._ret );
return params._ret;
}
@@ -2759,6 +2899,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput007_GetDigitalActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput007_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput007_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -2815,6 +2972,23 @@
};
TRACE("%p\n", _this);
STEAMCLIENT_CALL( ISteamInput_SteamInput007_GetAnalogActionOrigins, &params );
+ if (steaminput_report_xbox_identity())
+ {
+ struct ISteamInput_SteamInput007_TranslateActionOrigin_params translate_params =
+ {
+ .u_iface = _this->u_iface,
+ .eDestinationInputType = 3, /* k_ESteamInputType_XBoxOneController */
+ };
+ int32_t i;
+
+ for (i = 0; i < params._ret; i++)
+ {
+ if (!steaminput_origin_needs_xbox( originsOut[i] )) continue;
+ translate_params.eSourceOrigin = originsOut[i];
+ STEAMCLIENT_CALL( ISteamInput_SteamInput007_TranslateActionOrigin, &translate_params );
+ if (translate_params._ret) originsOut[i] = translate_params._ret;
+ }
+ }
return params._ret;
}
@@ -2981,6 +3155,7 @@
TRACE("%p\n", _this);
if (steaminput006_xinput_get_input_type( &params._ret, inputHandle, FALSE )) return params._ret;
STEAMCLIENT_CALL( ISteamInput_SteamInput007_GetInputTypeForHandle, &params );
+ params._ret = steaminput_native_input_type( params._ret );
return params._ret;
}
--- a/lsteamclient/steam_input_manual.c
+++ b/lsteamclient/steam_input_manual.c
@@ -31,9 +31,11 @@
enum steam_input_type
{
+ STEAM_INPUT_TYPE_STEAM_CONTROLLER = 1,
STEAM_INPUT_TYPE_XBOX_ONE = 3,
STEAM_INPUT_TYPE_PS4 = 5,
STEAM_INPUT_TYPE_PS5 = 13,
+ STEAM_INPUT_TYPE_STEAM_DECK = 14,
};
enum steam_input_configuration
@@ -774,6 +776,33 @@
return TRUE;
}
+/* Some games only have gamepad prompts for the controller types they know and
+ * fall back to keyboard prompts for a Steam Deck or Steam Controller. With
+ * PROTON_STEAM_INPUT_XBOX_PROMPTS=1, report those native devices as Xbox One
+ * controllers and let Steam translate their action origins to Xbox ones. */
+int steaminput_report_xbox_identity(void)
+{
+ static LONG enabled = -1;
+
+ if (ReadAcquire(&enabled) == -1)
+ InterlockedExchange(&enabled, env_enabled("PROTON_STEAM_INPUT_XBOX_PROMPTS"));
+ return ReadAcquire(&enabled);
+}
+
+uint32_t steaminput_native_input_type(uint32_t type)
+{
+ if (!steaminput_report_xbox_identity()) return type;
+ if (type == STEAM_INPUT_TYPE_STEAM_CONTROLLER || type == STEAM_INPUT_TYPE_STEAM_DECK)
+ return STEAM_INPUT_TYPE_XBOX_ONE;
+ return type;
+}
+
+int steaminput_origin_needs_xbox(uint32_t origin)
+{
+ /* k_EInputActionOrigin_SteamController_* and k_EInputActionOrigin_SteamDeck_* */
+ return (origin >= 1 && origin <= 49) || (origin >= 333 && origin <= 405);
+}
+
int steamcontroller_xinput_get_action_origin(uint32_t *origin, uint64_t input_handle, uint32_t xbox_origin,
BOOL legacy_sony)
{
--- a/lsteamclient/steamclient_private.h
+++ b/lsteamclient/steamclient_private.h
@@ -70,6 +70,9 @@
BOOL legacy_sony );
int steamcontroller_xinput_get_action_origin( uint32_t *origin, uint64_t input_handle, uint32_t xbox_origin,
BOOL legacy_sony );
+int steaminput_report_xbox_identity( void );
+uint32_t steaminput_native_input_type( uint32_t type );
+int steaminput_origin_needs_xbox( uint32_t origin );
#ifdef __cplusplus
}
@@ -17,7 +17,7 @@ Keep the real controller identity internally so direct USB haptics, controller a
3 files changed, 460 insertions(+), 18 deletions(-)
diff --git a/dlls/winebus.sys/bus_udev.c b/dlls/winebus.sys/bus_udev.c
index a2fab98..333bf96 100644
index 9922878df6ccaa77032b74c0e199b213b746f573..2e7729fd644e4962c671f5ef23882cd159dee38c 100644
--- a/dlls/winebus.sys/bus_udev.c
+++ b/dlls/winebus.sys/bus_udev.c
@@ -1512,6 +1512,13 @@ static BOOL enable_sony_hidraw_xinput(void)
@@ -47,7 +47,7 @@ index a2fab98..333bf96 100644
if (!desc.product[0] && ioctl(fd, HIDIOCGRAWNAME(sizeof(buffer) - 1), buffer) >= 0)
ntdll_umbstowcs(buffer, strlen(buffer) + 1, desc.product, ARRAY_SIZE(desc.product));
diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c
index a8699b2..da5e75a 100644
index 42f51d29cb5e6b3252007016ac3fa5d1c59b25ed..767f46637bbbfd6eeb7386e239d38c42dbb02da0 100644
--- a/dlls/winebus.sys/main.c
+++ b/dlls/winebus.sys/main.c
@@ -75,10 +75,32 @@ enum device_state
@@ -211,7 +211,7 @@ index a8699b2..da5e75a 100644
if (input_len) pos += swprintf(dst + pos, len - pos, input_format, ext->desc.input);
pos += 1;
dst[pos] = 0;
@@ -778,10 +889,89 @@ static BOOL deliver_next_report(struct device_extension *ext, IRP *irp)
@@ -778,6 +889,84 @@ static BOOL deliver_next_report(struct device_extension *ext, IRP *irp)
return TRUE;
}
@@ -296,13 +296,38 @@ index a8699b2..da5e75a 100644
static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD report_len)
{
struct device_extension *ext = (struct device_extension *)device->DeviceExtension;
- ULONG size = offsetof(struct hid_report, buffer[report_len]);
+ ULONG buffer_len = max(report_len, ext->desc.spoof_dualshock4 ? DUALSHOCK4_INPUT_REPORT_SIZE : 0);
+ ULONG size = offsetof(struct hid_report, buffer[buffer_len]);
struct hid_report *report, *last_report;
IRP *irp;
@@ -795,9 +984,12 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
return;
}
@@ -791,6 +981,14 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
- if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
+ /* Spoofed DualSense reports only match the DS4 descriptor once translated,
+ * so their declared report is looked up after the translation below. */
+ if (ext->desc.spoof_dualshock4) last_report = NULL;
+ else if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
else last_report = ext->last_reports[report_buf[0]];
- if (!last_report)
+ if (!last_report && !ext->desc.spoof_dualshock4)
{
WARN("Ignoring report with unexpected id %#x\n", *report_buf);
RtlLeaveCriticalSection(&ext->cs);
@@ -806,13 +998,13 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
/* Devices may send more data than their report declares, but the cached report
* and hidclass.sys buffers are only sized to hold the declared size. */
- if (report_len > last_report->length)
+ if (last_report && report_len > last_report->length)
{
WARN("overlong report %#x length %lu truncated to declared length %lu\n", *report_buf, report_len, last_report->length);
report_len = last_report->length;
}
- size = offsetof(struct hid_report, buffer[report_len]);
+ size = offsetof(struct hid_report, buffer[max(report_len, ext->desc.spoof_dualshock4 ? DUALSHOCK4_INPUT_REPORT_SIZE : 0)]);
if (!(report = RtlAllocateHeap(GetProcessHeap(), 0, size)))
{
RtlLeaveCriticalSection(&ext->cs);
@@ -821,6 +1013,29 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
memcpy(report->buffer, report_buf, report_len);
report->length = report_len;
@@ -310,45 +335,41 @@ index a8699b2..da5e75a 100644
+ * compatibility fixup reduces extended reports to their ten-byte form. */
+ if (!hidraw_fixup_dualsense_input_as_dualshock4(ext, report))
+ {
+ RtlLeaveCriticalSection(&ext->cs);
+ RtlFreeHeap(GetProcessHeap(), 0, report);
+ return;
+ }
+
+ if (ext->desc.spoof_dualshock4)
+ {
+ if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
+ else last_report = ext->last_reports[report->buffer[0]];
+ if (!last_report)
+ {
+ WARN("Ignoring report with unexpected id %#x\n", report->buffer[0]);
+ RtlLeaveCriticalSection(&ext->cs);
+ RtlFreeHeap(GetProcessHeap(), 0, report);
+ return;
+ }
+ report->length = min(report->length, last_report->length);
+ }
+
if (ext->report_fixups & HIDRAW_FIXUP_DUALSHOCK_BT)
{
/* As described in the Linux kernel driver, when connected over bluetooth, DS4 controllers
@@ -849,21 +1047,28 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
if (ext->state != DEVICE_STATE_STARTED)
{
RtlLeaveCriticalSection(&ext->cs);
+ RtlFreeHeap(GetProcessHeap(), 0, report);
return;
}
if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
+ else if (ext->desc.spoof_dualshock4) last_report = ext->last_reports[report->buffer[0]];
else last_report = ext->last_reports[report_buf[0]];
if (!last_report)
{
- WARN("Ignoring report with unexpected id %#x\n", *report_buf);
+ WARN("Ignoring report with unexpected id %#x\n",
+ ext->desc.spoof_dualshock4 ? report->buffer[0] : *report_buf);
RtlLeaveCriticalSection(&ext->cs);
+ RtlFreeHeap(GetProcessHeap(), 0, report);
return;
}
@@ -876,7 +1091,10 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
list_add_tail(&ext->reports, &report->entry);
- memcpy(last_report->buffer, report_buf, report_len);
+ if (ext->desc.spoof_dualshock4)
+ memcpy(last_report->buffer, report->buffer, min(report->length, last_report->length));
+ memcpy(last_report->buffer, report->buffer, report->length);
+ else
+ memcpy(last_report->buffer, report_buf, report_len);
if ((irp = pop_pending_read(ext)))
{
@@ -1020,6 +1225,32 @@ static NTSTATUS get_device_descriptors(UINT64 unix_device, BYTE **report_desc, U
@@ -1033,6 +1251,32 @@ static NTSTATUS get_device_descriptors(UINT64 unix_device, BYTE **report_desc, U
return STATUS_SUCCESS;
}
@@ -381,7 +402,7 @@ index a8699b2..da5e75a 100644
static USAGE_AND_PAGE get_device_usages(UINT64 unix_device, UINT *buttons)
{
HIDP_DEVICE_DESC device_desc;
@@ -1524,8 +1755,8 @@ static NTSTATUS pdo_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1537,8 +1781,8 @@ static NTSTATUS pdo_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
else if (ext->state == DEVICE_STATE_REMOVED) status = STATUS_DELETE_PENDING;
else if ((status = unix_device_start(device)))
ERR("Failed to start device %p, status %#lx\n", device, status);
@@ -392,7 +413,7 @@ index a8699b2..da5e75a 100644
{
status = STATUS_SUCCESS;
reports = ext->collection_desc.ReportIDs;
@@ -1604,6 +1835,7 @@ static NTSTATUS WINAPI common_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1617,6 +1861,7 @@ static NTSTATUS WINAPI common_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
static NTSTATUS hid_get_device_string(DEVICE_OBJECT *device, DWORD index, WCHAR *buffer, DWORD buffer_len)
{
struct device_extension *ext = (struct device_extension *)device->DeviceExtension;
@@ -400,7 +421,7 @@ index a8699b2..da5e75a 100644
DWORD len;
switch (index)
@@ -1614,9 +1846,10 @@ static NTSTATUS hid_get_device_string(DEVICE_OBJECT *device, DWORD index, WCHAR
@@ -1627,9 +1872,10 @@ static NTSTATUS hid_get_device_string(DEVICE_OBJECT *device, DWORD index, WCHAR
else memcpy(buffer, ext->desc.manufacturer, len);
return STATUS_SUCCESS;
case HID_STRING_ID_IPRODUCT:
@@ -413,7 +434,7 @@ index a8699b2..da5e75a 100644
return STATUS_SUCCESS;
case HID_STRING_ID_ISERIALNUMBER:
len = (wcslen(ext->desc.serialnumber) + 1) * sizeof(WCHAR);
@@ -1642,7 +1875,7 @@ static void hidraw_disable_report_fixups(DEVICE_OBJECT *device)
@@ -1655,7 +1901,7 @@ static void hidraw_disable_report_fixups(DEVICE_OBJECT *device)
/* XInput consumes the translated basic input report even after sending a
* valid Bluetooth output report, which switches Sony controllers to their
* extended input report. */
@@ -422,7 +443,7 @@ index a8699b2..da5e75a 100644
/* FIXME: we may want to validate CRC at the end of the outbound HID reports,
* as controllers do not switch modes if it is incorrect.
@@ -1668,6 +1901,194 @@ static BYTE dualsense_hid_haptics_intensity_to_motor(BYTE lo, BYTE hi)
@@ -1681,6 +1927,194 @@ static BYTE dualsense_hid_haptics_intensity_to_motor(BYTE lo, BYTE hi)
return value <= 0xff ? value : value / 257;
}
@@ -617,7 +638,7 @@ index a8699b2..da5e75a 100644
static BOOL hidraw_fixup_dualsense_usb_output_report(struct device_extension *ext,
HID_XFER_PACKET *packet, HID_XFER_PACKET *fixed_packet, BYTE *fixed_report)
{
@@ -1749,8 +2170,8 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1762,8 +2196,8 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
memset(attr, 0, sizeof(*attr));
attr->Size = sizeof(*attr);
attr->VendorID = ext->desc.vid;
@@ -628,7 +649,7 @@ index a8699b2..da5e75a 100644
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = sizeof(*attr);
@@ -1834,7 +2255,7 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1847,7 +2281,7 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
{
HID_XFER_PACKET *packet = (HID_XFER_PACKET *)irp->UserBuffer;
HID_XFER_PACKET fixed_packet;
@@ -637,7 +658,7 @@ index a8699b2..da5e75a 100644
if (TRACE_ON(hid))
{
TRACE("write output report id %u length %lu:\n", packet->reportId, packet->reportBufferLen);
@@ -1847,7 +2268,9 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1860,7 +2294,9 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
TRACE("%s\n", buffer);
}
}
@@ -648,7 +669,7 @@ index a8699b2..da5e75a 100644
packet = &fixed_packet;
unix_device_set_output_report(device, packet, &irp->IoStatus);
if (!irp->IoStatus.Status) hidraw_disable_report_fixups(device);
@@ -1856,8 +2279,11 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1869,8 +2305,11 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
case IOCTL_HID_GET_FEATURE:
{
HID_XFER_PACKET *packet = (HID_XFER_PACKET *)irp->UserBuffer;
@@ -662,7 +683,7 @@ index a8699b2..da5e75a 100644
if (!irp->IoStatus.Status && TRACE_ON(hid))
{
TRACE("read feature report id %u length %lu:\n", packet->reportId, packet->reportBufferLen);
@@ -1887,8 +2313,11 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -1900,8 +2339,11 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
TRACE("%s\n", buffer);
}
}
@@ -677,7 +698,7 @@ index a8699b2..da5e75a 100644
}
default:
diff --git a/dlls/winebus.sys/unixlib.h b/dlls/winebus.sys/unixlib.h
index ad3b268..562dd46 100644
index ad3b268b0b679e9c3d612e1fca2a8ef921a0fb70..562dd46fe787528cb1b2db6483ee65b70a914e25 100644
--- a/dlls/winebus.sys/unixlib.h
+++ b/dlls/winebus.sys/unixlib.h
@@ -41,6 +41,7 @@ struct device_desc
@@ -18,7 +18,7 @@ needed while retaining the physical identity internally for audio,
haptics, containers, and hotplug handling.
---
diff --git a/dlls/winebus.sys/bus_udev.c b/dlls/winebus.sys/bus_udev.c
index 2e7729fd..c516d803 100644
index 2e7729fd644e4962c671f5ef23882cd159dee38c..c516d8036b107536a7e4598897039f51993e51e3 100644
--- a/dlls/winebus.sys/bus_udev.c
+++ b/dlls/winebus.sys/bus_udev.c
@@ -1519,6 +1519,13 @@ static BOOL spoof_dualsense_as_dualshock4(void)
@@ -54,7 +54,7 @@ index 2e7729fd..c516d803 100644
if (!desc.product[0] && ioctl(fd, HIDIOCGRAWNAME(sizeof(buffer) - 1), buffer) >= 0)
diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c
index 1201f814..d5bc6028 100644
index 83d87480651c5cbd66911c28273f18a2ca48ba5d..97a884197b40e8d0f1b59a09b25fe461f1891d7c 100644
--- a/dlls/winebus.sys/main.c
+++ b/dlls/winebus.sys/main.c
@@ -76,8 +76,16 @@ enum device_state
@@ -169,7 +169,7 @@ index 1201f814..d5bc6028 100644
return ext->desc.product;
}
@@ -1050,10 +1117,47 @@ done:
@@ -1050,9 +1117,45 @@ done:
return TRUE;
}
@@ -211,52 +211,62 @@ index 1201f814..d5bc6028 100644
static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD report_len)
{
struct device_extension *ext = (struct device_extension *)device->DeviceExtension;
- ULONG buffer_len = max(report_len, ext->desc.spoof_dualshock4 ? DUALSHOCK4_INPUT_REPORT_SIZE : 0);
+ ULONG buffer_len = max(report_len,
+ (ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1)
+ ? DUALSHOCK4_INPUT_REPORT_SIZE : 0);
ULONG size = offsetof(struct hid_report, buffer[buffer_len]);
+ BOOL translate = ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1;
struct hid_report *report, *last_report;
ULONG size;
IRP *irp;
@@ -1071,6 +1175,11 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
@@ -1067,12 +1170,12 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
return;
}
- /* Spoofed DualSense reports only match the DS4 descriptor once translated,
- * so their declared report is looked up after the translation below. */
- if (ext->desc.spoof_dualshock4) last_report = NULL;
+ /* Spoofed reports only match the DS4 descriptor once translated, so
+ * their declared report is looked up after the translation below. */
+ if (translate) last_report = NULL;
else if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
else last_report = ext->last_reports[report_buf[0]];
- if (!last_report && !ext->desc.spoof_dualshock4)
+ if (!last_report && !translate)
{
WARN("Ignoring report with unexpected id %#x\n", *report_buf);
RtlLeaveCriticalSection(&ext->cs);
@@ -1087,7 +1190,7 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
report_len = last_report->length;
}
- size = offsetof(struct hid_report, buffer[max(report_len, ext->desc.spoof_dualshock4 ? DUALSHOCK4_INPUT_REPORT_SIZE : 0)]);
+ size = offsetof(struct hid_report, buffer[max(report_len, translate ? DUALSHOCK4_INPUT_REPORT_SIZE : 0)]);
if (!(report = RtlAllocateHeap(GetProcessHeap(), 0, size)))
{
RtlLeaveCriticalSection(&ext->cs);
@@ -1104,8 +1207,14 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
RtlFreeHeap(GetProcessHeap(), 0, report);
return;
}
+ if (!hidraw_fixup_dualshock4_v2_input_as_v1(ext, report))
+ {
+ RtlLeaveCriticalSection(&ext->cs);
+ RtlFreeHeap(GetProcessHeap(), 0, report);
+ return;
+ }
if (ext->report_fixups & HIDRAW_FIXUP_DUALSHOCK_BT)
- if (ext->desc.spoof_dualshock4)
+ if (translate)
{
@@ -1135,12 +1244,14 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
}
if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
- else if (ext->desc.spoof_dualshock4) last_report = ext->last_reports[report->buffer[0]];
+ else if (ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1)
+ last_report = ext->last_reports[report->buffer[0]];
else last_report = ext->last_reports[report_buf[0]];
if (!last_report)
{
WARN("Ignoring report with unexpected id %#x\n",
- ext->desc.spoof_dualshock4 ? report->buffer[0] : *report_buf);
+ (ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1)
+ ? report->buffer[0] : *report_buf);
RtlLeaveCriticalSection(&ext->cs);
RtlFreeHeap(GetProcessHeap(), 0, report);
return;
@@ -1148,7 +1259,7 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
if (!ext->collection_desc.ReportIDs[0].ReportID) last_report = ext->last_reports[0];
else last_report = ext->last_reports[report->buffer[0]];
@@ -1174,7 +1283,7 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
list_add_tail(&ext->reports, &report->entry);
- if (ext->desc.spoof_dualshock4)
+ if (ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1)
memcpy(last_report->buffer, report->buffer, min(report->length, last_report->length));
+ if (translate)
memcpy(last_report->buffer, report->buffer, report->length);
else
memcpy(last_report->buffer, report_buf, report_len);
@@ -1311,15 +1422,28 @@ static NTSTATUS get_device_descriptors(UINT64 unix_device, BYTE **report_desc, U
@@ -1337,15 +1446,28 @@ static NTSTATUS get_device_descriptors(UINT64 unix_device, BYTE **report_desc, U
static NTSTATUS get_windows_device_descriptors(struct device_extension *ext, BYTE **report_desc,
UINT *report_desc_length, HIDP_DEVICE_DESC *device_desc)
{
@@ -288,7 +298,7 @@ index 1201f814..d5bc6028 100644
status = HidP_GetCollectionDescription(*report_desc, *report_desc_length, PagedPool, device_desc);
if (status != HIDP_STATUS_SUCCESS)
@@ -1330,7 +1454,8 @@ static NTSTATUS get_windows_device_descriptors(struct device_extension *ext, BYT
@@ -1356,7 +1478,8 @@ static NTSTATUS get_windows_device_descriptors(struct device_extension *ext, BYT
return status;
}
@@ -298,7 +308,7 @@ index 1201f814..d5bc6028 100644
return STATUS_SUCCESS;
}
@@ -1958,7 +2083,8 @@ static void hidraw_disable_report_fixups(DEVICE_OBJECT *device)
@@ -1984,7 +2107,8 @@ static void hidraw_disable_report_fixups(DEVICE_OBJECT *device)
/* XInput consumes the translated basic input report even after sending a
* valid Bluetooth output report, which switches Sony controllers to their
* extended input report. */
@@ -308,7 +318,7 @@ index 1201f814..d5bc6028 100644
/* FIXME: we may want to validate CRC at the end of the outbound HID reports,
* as controllers do not switch modes if it is incorrect.
@@ -2001,6 +2127,8 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2027,6 +2151,8 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
struct device_extension *ext = (struct device_extension *)device->DeviceExtension;
@@ -317,7 +327,7 @@ index 1201f814..d5bc6028 100644
BYTE native_report[DUALSENSE_FEATURE_REPORT_FIRMWARE_INFO_SIZE] = {0};
HID_XFER_PACKET native_packet =
{
@@ -2009,12 +2137,24 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2035,12 +2161,24 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
IO_STATUS_BLOCK native_io = {0};
ULONG response_size;
@@ -344,7 +354,7 @@ index 1201f814..d5bc6028 100644
{
io->Information = 0;
io->Status = STATUS_BUFFER_TOO_SMALL;
@@ -2022,28 +2162,43 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2048,28 +2186,43 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
}
memset(packet->reportBuffer, 0, packet->reportBufferLen);
@@ -404,7 +414,7 @@ index 1201f814..d5bc6028 100644
}
if (packet->reportBufferLen < response_size)
@@ -2057,7 +2212,7 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2083,7 +2236,7 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
unix_device_get_feature_report(device, &native_packet, &native_io);
if (native_io.Status)
{
@@ -413,7 +423,7 @@ index 1201f814..d5bc6028 100644
native_packet.reportId, packet->reportId, native_io.Status);
*io = native_io;
return TRUE;
@@ -2069,8 +2224,21 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2095,8 +2248,21 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
switch (packet->reportId)
{
case DUALSHOCK4_FEATURE_REPORT_CALIBRATION:
@@ -437,7 +447,7 @@ index 1201f814..d5bc6028 100644
break;
case DUALSHOCK4_FEATURE_REPORT_PAIRING_INFO:
/* Both reports store the controller MAC in little-endian order at offset 1. */
@@ -2081,7 +2249,7 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2107,7 +2273,7 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
io->Information = response_size;
io->Status = STATUS_SUCCESS;
@@ -446,7 +456,7 @@ index 1201f814..d5bc6028 100644
native_packet.reportId, packet->reportId);
return TRUE;
}
@@ -2115,13 +2283,36 @@ static BOOL hidraw_fixup_spoofed_dualshock4_output_report(struct device_extensio
@@ -2141,13 +2307,36 @@ static BOOL hidraw_fixup_spoofed_dualshock4_output_report(struct device_extensio
{
BYTE *common;
@@ -484,7 +494,7 @@ index 1201f814..d5bc6028 100644
memset(fixed_report, 0, DUALSENSE_OUTPUT_REPORT_BT_SIZE);
*fixed_packet = *packet;
@@ -2254,7 +2445,8 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -2280,7 +2469,8 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
attr->Size = sizeof(*attr);
attr->VendorID = ext->desc.vid;
attr->ProductID = get_windows_product_id(ext);
@@ -495,7 +505,7 @@ index 1201f814..d5bc6028 100644
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = sizeof(*attr);
diff --git a/dlls/winebus.sys/unixlib.h b/dlls/winebus.sys/unixlib.h
index 562dd46f..6ac652ea 100644
index 562dd46fe787528cb1b2db6483ee65b70a914e25..6ac652eac8180c0aee07992e654cd345a0998768 100644
--- a/dlls/winebus.sys/unixlib.h
+++ b/dlls/winebus.sys/unixlib.h
@@ -42,6 +42,7 @@ struct device_desc
@@ -506,4 +516,3 @@ index 562dd46f..6ac652ea 100644
WCHAR manufacturer[MAX_PATH];
WCHAR product[MAX_PATH];
@@ -16,7 +16,7 @@ When a registered four-channel Sony effect carries real speaker or actuator data
4 files changed, 398 insertions(+), 9 deletions(-)
diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c
index e851ff8..f4bd018 100644
index 434e167e4a7f6668dc592b74693a0da429760177..7fce1c4cafe5728c75c851c59c4dcf68ccb96eab 100644
--- a/dlls/winebus.sys/main.c
+++ b/dlls/winebus.sys/main.c
@@ -37,6 +37,7 @@
@@ -226,8 +226,8 @@ index e851ff8..f4bd018 100644
winebus_call(device_remove, &params);
}
@@ -1298,6 +1475,9 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
return;
@@ -1322,6 +1499,9 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
}
}
+ if (sony_controls_became_active(ext, report_buf, report_len))
@@ -235,8 +235,8 @@ index e851ff8..f4bd018 100644
+
list_add_tail(&ext->reports, &report->entry);
if (ext->desc.spoof_dualshock4 || ext->desc.spoof_dualshock4_v1)
@@ -2672,6 +2852,7 @@ static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *p
if (translate)
@@ -2696,6 +2876,7 @@ static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *p
static void WINAPI driver_unload(DRIVER_OBJECT *driver)
{
@@ -244,7 +244,7 @@ index e851ff8..f4bd018 100644
NtClose(driver_key);
}
@@ -2684,6 +2865,9 @@ NTSTATUS WINAPI DriverEntry( DRIVER_OBJECT *driver, UNICODE_STRING *path )
@@ -2708,6 +2889,9 @@ NTSTATUS WINAPI DriverEntry( DRIVER_OBJECT *driver, UNICODE_STRING *path )
if ((ret = __wine_init_unix_call())) return ret;
@@ -255,7 +255,7 @@ index e851ff8..f4bd018 100644
attr.ObjectName = path;
attr.Attributes = OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE;
diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c
index 817d487..baa961d 100644
index bc899fea6f132644ec2bcce14a32007c5b68fee4..df5ae6495bf9d8f115e7089771b4714386360c48 100644
--- a/dlls/winepulse.drv/pulse.c
+++ b/dlls/winepulse.drv/pulse.c
@@ -54,6 +54,7 @@
@@ -500,7 +500,7 @@ index 817d487..baa961d 100644
stream->haptic_alsa_path[0])
{
diff --git a/server/device.c b/server/device.c
index 3eed743..cfd76e4 100644
index 3eed74343e557cacb022d7b4ca77fc31f59df278..cfd76e4633012c9a40a6b241d595d88f175c7efb 100644
--- a/server/device.c
+++ b/server/device.c
@@ -39,6 +39,9 @@
@@ -556,10 +556,10 @@ index 3eed743..cfd76e4 100644
DECL_HANDLER(create_device)
{
diff --git a/server/protocol.def b/server/protocol.def
index 2047b40..8ea4cc7 100644
index e7a8a3b27ab841e6ce4b6189ef547be045f5adce..4eb22fb2bb192d8a65eecbce030d73cf9f245264 100644
--- a/server/protocol.def
+++ b/server/protocol.def
@@ -3809,6 +3809,27 @@ struct handle_info
@@ -3922,6 +3922,27 @@ struct handle_info
@END
@@ -36,7 +36,7 @@ assignments without changing the USB identity, so that incompatible
layout cannot be distinguished by VID/PID and is not handled here.
---
diff --git a/dlls/winebus.sys/bus_udev.c b/dlls/winebus.sys/bus_udev.c
index c516d80..9ec7763 100644
index c516d8036b107536a7e4598897039f51993e51e3..9ec7763619bb090a681ec4c94a134eea1792d4dc 100644
--- a/dlls/winebus.sys/bus_udev.c
+++ b/dlls/winebus.sys/bus_udev.c
@@ -1541,7 +1541,14 @@ static NTSTATUS hidraw_device_create(struct udev_device *dev, int fd, const char
@@ -56,10 +56,10 @@ index c516d80..9ec7763 100644
desc.spoof_dualshock4 = TRUE;
desc.spoof_dualshock4_v1 = spoof_dualshock4_v2_as_v1();
diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c
index 9ab4adf..e72306b 100644
index 7fce1c4cafe5728c75c851c59c4dcf68ccb96eab..367e0dc0445f7fc84444efca18e25e682c72b1f9 100644
--- a/dlls/winebus.sys/main.c
+++ b/dlls/winebus.sys/main.c
@@ -1160,6 +1160,7 @@ static BOOL is_hidraw_enabled(WORD vid, WORD pid, const USAGE_AND_PAGE *usages,
@@ -1158,6 +1158,7 @@ static BOOL is_hidraw_enabled(WORD vid, WORD pid, const USAGE_AND_PAGE *usages,
if (options.disable_sdl && options.disable_input) prefer_hidraw = TRUE;
if (is_dualshock4_gamepad(vid, pid)) prefer_hidraw = TRUE;
if (is_dualsense_gamepad(vid, pid)) prefer_hidraw = TRUE;
@@ -67,7 +67,7 @@ index 9ab4adf..e72306b 100644
switch (vid)
{
@@ -1259,7 +1260,42 @@ static BOOL deliver_next_report(struct device_extension *ext, IRP *irp)
@@ -1257,7 +1258,42 @@ static BOOL deliver_next_report(struct device_extension *ext, IRP *irp)
return TRUE;
}
@@ -111,7 +111,7 @@ index 9ab4adf..e72306b 100644
struct hid_report *report)
{
BYTE translated[DUALSHOCK4_INPUT_REPORT_SIZE] = {0};
@@ -1268,6 +1304,17 @@ static BOOL hidraw_fixup_dualsense_input_as_dualshock4(struct device_extension *
@@ -1266,6 +1302,17 @@ static BOOL hidraw_fixup_dualsense_input_as_dualshock4(struct device_extension *
if (!ext->desc.spoof_dualshock4) return TRUE;
@@ -129,16 +129,16 @@ index 9ab4adf..e72306b 100644
if (report->buffer[0] == DUALSENSE_INPUT_REPORT_USB &&
ext->desc.bus_type == BUS_TYPE_BLUETOOTH && report->length >= 10)
{
@@ -1390,7 +1437,7 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
@@ -1419,7 +1466,7 @@ static void process_hid_report(DEVICE_OBJECT *device, BYTE *report_buf, DWORD re
/* Preserve the complete DualSense state before the generic Bluetooth
* compatibility fixup reduces extended reports to their ten-byte form. */
- if (!hidraw_fixup_dualsense_input_as_dualshock4(ext, report))
+ if (!hidraw_fixup_input_as_dualshock4(ext, report))
{
RtlLeaveCriticalSection(&ext->cs);
RtlFreeHeap(GetProcessHeap(), 0, report);
return;
@@ -2241,6 +2288,12 @@ static NTSTATUS pdo_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -2222,6 +2269,12 @@ static NTSTATUS pdo_pnp_dispatch(DEVICE_OBJECT *device, IRP *irp)
{
report->length = reports[i].InputLength;
report->buffer[0] = reports[i].ReportID;
@@ -151,7 +151,7 @@ index 9ab4adf..e72306b 100644
ext->last_reports[reports[i].ReportID] = report;
}
}
@@ -2387,6 +2440,54 @@ static void sony_set_output_report_crc(BYTE *report, ULONG report_len)
@@ -2368,6 +2421,54 @@ static void sony_set_output_report_crc(BYTE *report, ULONG report_len)
report[report_len - 1] = crc >> 24;
}
@@ -206,7 +206,7 @@ index 9ab4adf..e72306b 100644
static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
@@ -2434,6 +2535,12 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2415,6 +2516,12 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
return TRUE;
}
@@ -219,7 +219,7 @@ index 9ab4adf..e72306b 100644
if (!ext->desc.spoof_dualshock4)
{
/* DS4 v1 and v2 share their report layout. Bluetooth calibration is
@@ -2521,10 +2628,17 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
@@ -2502,10 +2609,17 @@ static BOOL hidraw_get_spoofed_dualshock4_feature_report(DEVICE_OBJECT *device,
static BOOL hidraw_set_spoofed_dualshock4_feature_report(struct device_extension *ext,
HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
@@ -239,7 +239,7 @@ index 9ab4adf..e72306b 100644
if (packet->reportBufferLen != DUALSHOCK4_FEATURE_REPORT_USB_INIT_SIZE ||
packet->reportBuffer[0] != DUALSHOCK4_FEATURE_REPORT_USB_INIT)
{
@@ -2795,6 +2909,22 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
@@ -2776,6 +2890,22 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
HID_XFER_PACKET *packet = (HID_XFER_PACKET *)irp->UserBuffer;
HID_XFER_PACKET fixed_packet;
BYTE fixed_report[DUALSENSE_OUTPUT_REPORT_BT_SIZE];
@@ -263,7 +263,7 @@ index 9ab4adf..e72306b 100644
{
TRACE("write output report id %u length %lu:\n", packet->reportId, packet->reportBufferLen);
diff --git a/dlls/winebus.sys/unixlib.h b/dlls/winebus.sys/unixlib.h
index 6ac652e..3074967 100644
index 6ac652eac8180c0aee07992e654cd345a0998768..3074967e9eac3877fd78c6651263f361e2857c79 100644
--- a/dlls/winebus.sys/unixlib.h
+++ b/dlls/winebus.sys/unixlib.h
@@ -191,6 +191,11 @@ static inline BOOL is_xbox_gamepad(WORD vid, WORD pid)
@@ -22,7 +22,7 @@ Include HID-class tests with two synthetic Sony collections. These tests
and the runtime integration still require validation in a built tree.
---
diff --git a/dlls/dinput/tests/hid.c b/dlls/dinput/tests/hid.c
index 7c42ac54f74..cdb0907fb14 100644
index 7c42ac54f748e34ce8c003130c8b2e103f35aed9..cdb0907fb14208a2b1ba0b3b973e2bade1290a4d 100644
--- a/dlls/dinput/tests/hid.c
+++ b/dlls/dinput/tests/hid.c
@@ -58,6 +58,7 @@
@@ -208,7 +208,7 @@ index 7c42ac54f74..cdb0907fb14 100644
done:
bus_device_stop();
diff --git a/dlls/hidclass.sys/device.c b/dlls/hidclass.sys/device.c
index 563d1ee22d2..d4c3f33a6c1 100644
index 563d1ee22d27480d53c8564fdb22e1dc6af2aca0..d4c3f33a6c171bf682cf63c0f1482cc6d39db03e 100644
--- a/dlls/hidclass.sys/device.c
+++ b/dlls/hidclass.sys/device.c
@@ -27,6 +27,7 @@
@@ -401,7 +401,7 @@ index 563d1ee22d2..d4c3f33a6c1 100644
hid_queue_destroy( queue );
}
diff --git a/dlls/hidclass.sys/hid.h b/dlls/hidclass.sys/hid.h
index 21de22161e4..76c36dcec80 100644
index 21de22161e4456c11107d3b9c6f4809ab03e0628..76c36dcec807a3b845a5747d876c5ea35e1b0d71 100644
--- a/dlls/hidclass.sys/hid.h
+++ b/dlls/hidclass.sys/hid.h
@@ -23,7 +23,7 @@
@@ -425,7 +425,7 @@ index 21de22161e4..76c36dcec80 100644
typedef struct _minidriver
diff --git a/dlls/win32u/rawinput.c b/dlls/win32u/rawinput.c
index 25ddb68e4c1..8f804386e8c 100644
index 25ddb68e4c1bb0a71f754cb4358cd7edafb57954..8f804386e8cb9c0e8c0c1ab048a88e8fcdd3d17c 100644
--- a/dlls/win32u/rawinput.c
+++ b/dlls/win32u/rawinput.c
@@ -67,6 +67,23 @@ static unsigned int registered_device_count;
@@ -591,7 +591,7 @@ index 25ddb68e4c1..8f804386e8c 100644
{
memmove( pos, pos + 1, (char *)end - (char *)(pos + 1) );
diff --git a/dlls/xinput1_3/main.c b/dlls/xinput1_3/main.c
index 1a06300c9de..222748df813 100644
index 2fefed4e3d6bfecfc6b967e9d9ce6f949c0ed7e4..1014fe11b85fd0be85a43014d03c6987e83fa91e 100644
--- a/dlls/xinput1_3/main.c
+++ b/dlls/xinput1_3/main.c
@@ -24,6 +24,8 @@
@@ -627,7 +627,7 @@ index 1a06300c9de..222748df813 100644
struct
{
@@ -121,9 +127,13 @@ static HMODULE xinput_instance;
@@ -121,12 +127,16 @@ static HMODULE xinput_instance;
static HANDLE start_event;
static HANDLE update_event;
static HANDLE steam_overlay_event;
@@ -635,13 +635,16 @@ index 1a06300c9de..222748df813 100644
+static BOOL auto_sony_xinput;
+static LONG steam_virtual_present;
static HKEY deviface_key;
static HANDLE key_changed_event;
static SRWLOCK state_lock = SRWLOCK_INIT;
static XINPUT_STATE current_state[XUSER_MAX_COUNT];
+static HANDLE current_native_input_event[XUSER_MAX_COUNT];
static BOOL persistent_player1;
static XINPUT_STATE persistent_state;
@@ -131,9 +141,60 @@ DWORD WINAPI __wine_XInputGetDeviceVidPid(DWORD index, WORD *vendor_id, WORD *pr
@@ -134,9 +144,60 @@ DWORD WINAPI __wine_XInputGetDeviceVidPid(DWORD index, WORD *vendor_id, WORD *pr
DWORD WINAPI __wine_XInputGetSonyProductId(DWORD index, WORD *product_id);
static USHORT sony_gamepad_product_id(const WCHAR *path);
@@ -702,7 +705,7 @@ index 1a06300c9de..222748df813 100644
if (!state) memset(current_state + index, 0, sizeof(*state));
else
{
@@ -145,11 +206,17 @@ static void set_current_state(UINT index, const XINPUT_STATE *state)
@@ -148,11 +209,17 @@ static void set_current_state(UINT index, const XINPUT_STATE *state)
static BOOL get_current_state(UINT index, XINPUT_STATE *state)
{
@@ -721,7 +724,7 @@ index 1a06300c9de..222748df813 100644
}
static BOOL is_persistent_player1(DWORD index)
@@ -455,7 +522,7 @@ static void controller_disable(struct xinput_controller *controller)
@@ -458,7 +525,7 @@ static void controller_disable(struct xinput_controller *controller)
XINPUT_VIBRATION state = {0};
if (!controller->enabled) return;
@@ -730,7 +733,7 @@ index 1a06300c9de..222748df813 100644
controller->enabled = FALSE;
if (CancelIoEx(controller->device, &controller->hid.read_ovl))
@@ -477,6 +544,8 @@ static void controller_destroy(struct xinput_controller *controller, BOOL alread
@@ -480,6 +547,8 @@ static void controller_destroy(struct xinput_controller *controller, BOOL alread
CloseHandle(controller->device);
controller->device = NULL;
@@ -739,7 +742,7 @@ index 1a06300c9de..222748df813 100644
free(controller->hid.input_report_buf);
free(controller->hid.output_report_buf);
@@ -495,7 +564,7 @@ static BOOL controller_enable(struct xinput_controller *controller)
@@ -498,7 +567,7 @@ static BOOL controller_enable(struct xinput_controller *controller)
BOOL ret;
if (controller->enabled) return TRUE;
@@ -748,7 +751,7 @@ index 1a06300c9de..222748df813 100644
memset(&controller->hid.read_ovl, 0, sizeof(controller->hid.read_ovl));
controller->hid.read_ovl.hEvent = controller->read_event;
@@ -592,19 +661,38 @@ static BOOL device_is_overridden(HANDLE device)
@@ -595,19 +664,38 @@ static BOOL device_is_overridden(HANDLE device)
}
/* open a device with the given path at the given index, xinput_cs must be held */
@@ -789,7 +792,7 @@ index 1a06300c9de..222748df813 100644
if (!HidD_GetPreparsedData(device, &preparsed))
WARN("ignoring HID device, HidD_GetPreparsedData failed with error %lu\n", GetLastError());
else if ((status = HidP_GetCaps(preparsed, &caps)) != HIDP_STATUS_SUCCESS)
@@ -620,10 +708,13 @@ static BOOL open_device_at_index(const WCHAR *device_path, int index)
@@ -623,10 +711,13 @@ static BOOL open_device_at_index(const WCHAR *device_path, int index)
WARN("ignoring HID device, failed to initialize\n");
else
{
@@ -804,7 +807,7 @@ index 1a06300c9de..222748df813 100644
CloseHandle(device);
HidD_FreePreparsedData(preparsed);
return TRUE;
@@ -649,8 +740,9 @@ static BOOL find_opened_device(const WCHAR *device_path, int *slot)
@@ -652,8 +743,9 @@ static BOOL find_opened_device(const WCHAR *device_path, int *slot)
swscanf(device_path, L"\\\\?\\HID#VID_28DE&PID_11FF&XI_%02u#", &i) == 1) &&
i < XUSER_MAX_COUNT && *slot != i)
{
@@ -815,7 +818,7 @@ index 1a06300c9de..222748df813 100644
*slot = i;
}
@@ -658,14 +750,20 @@ static BOOL find_opened_device(const WCHAR *device_path, int *slot)
@@ -661,14 +753,20 @@ static BOOL find_opened_device(const WCHAR *device_path, int *slot)
}
/* try opening a new controller device from the given path, xinput_cs must be held */
@@ -839,7 +842,7 @@ index 1a06300c9de..222748df813 100644
}
/* try closing an open controller device with the given path, xinput_cs must be held */
@@ -685,6 +783,7 @@ static void update_controller_list(void)
@@ -688,16 +786,15 @@ static void update_controller_list(void)
GUID guid = GUID_DEVINTERFACE_WINEXINPUT;
HDEVINFO set;
DWORD idx;
@@ -847,7 +850,31 @@ index 1a06300c9de..222748df813 100644
EnterCriticalSection(&xinput_cs);
@@ -696,11 +795,30 @@ static void update_controller_list(void)
+ /* The WINEXINPUT key doesn't cover the HID class, so an unchanged or missing
+ * key only skips the WINEXINPUT pass, not automatic Sony discovery. */
if (deviface_key)
{
- if (WaitForSingleObject(key_changed_event, 0) == WAIT_TIMEOUT)
- {
- LeaveCriticalSection(&xinput_cs);
- return;
- }
+ if (WaitForSingleObject(key_changed_event, 0) == WAIT_TIMEOUT) goto automatic_sony;
TRACE("key changed.\n");
}
else
@@ -712,8 +809,8 @@ static void update_controller_list(void)
if (RegOpenKeyW(HKEY_LOCAL_MACHINE, keystr, &deviface_key))
{
TRACE("Failed to open key %s.\n", debugstr_w(keystr));
- LeaveCriticalSection(&xinput_cs);
- return;
+ deviface_key = NULL;
+ goto automatic_sony;
}
key_changed_event = CreateEventW(NULL, FALSE, FALSE, NULL);
TRACE("opened key %p, event %p.\n", deviface_key, key_changed_event);
@@ -729,11 +826,31 @@ static void update_controller_list(void)
{
if (!SetupDiGetDeviceInterfaceDetailW(set, &iface, detail, sizeof(buffer), NULL, NULL))
continue;
@@ -864,6 +891,7 @@ index 1a06300c9de..222748df813 100644
SetupDiDestroyDeviceInfoList(set);
+ InterlockedExchange(&steam_virtual_present, native_present);
+
+automatic_sony:
+ if (auto_sony_xinput && !steam_virtual_present)
+ {
+ HidD_GetHidGuid(&guid);
@@ -880,7 +908,7 @@ index 1a06300c9de..222748df813 100644
LeaveCriticalSection(&xinput_cs);
}
@@ -887,9 +1005,22 @@ static LRESULT CALLBACK xinput_devnotify_wndproc(HWND hwnd, UINT msg, WPARAM wpa
@@ -920,9 +1037,22 @@ static LRESULT CALLBACK xinput_devnotify_wndproc(HWND hwnd, UINT msg, WPARAM wpa
{
DEV_BROADCAST_DEVICEINTERFACE_W *iface = (DEV_BROADCAST_DEVICEINTERFACE_W *)lparam;
@@ -905,7 +933,7 @@ index 1a06300c9de..222748df813 100644
LeaveCriticalSection(&xinput_cs);
}
@@ -914,7 +1045,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
@@ -947,7 +1077,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
.lpszClassName = L"__wine_xinput_devnotify",
.lpfnWndProc = xinput_devnotify_wndproc,
};
@@ -914,7 +942,7 @@ index 1a06300c9de..222748df813 100644
HWND hwnd;
MSG msg;
@@ -924,6 +1055,11 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
@@ -957,6 +1087,11 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
hwnd = CreateWindowExW(0, cls.lpszClassName, NULL, 0, 0, 0, 0, 0,
HWND_MESSAGE, NULL, NULL, NULL);
notif = RegisterDeviceNotificationW(hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE);
@@ -926,7 +954,7 @@ index 1a06300c9de..222748df813 100644
update_controller_list();
SetEvent(start_event);
@@ -953,6 +1089,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
@@ -986,6 +1121,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
ERR("wait failed in the update thread, ret %lu, error %lu\n", ret, GetLastError());
UnregisterDeviceNotification(notif);
@@ -934,7 +962,7 @@ index 1a06300c9de..222748df813 100644
DestroyWindow(hwnd);
UnregisterClassW(cls.lpszClassName, xinput_instance);
@@ -961,6 +1098,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
@@ -994,6 +1130,7 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
static BOOL WINAPI start_update_thread_once( INIT_ONCE *once, void *param, void **context )
{
@@ -942,7 +970,7 @@ index 1a06300c9de..222748df813 100644
HANDLE thread;
HMODULE module;
int i;
@@ -969,6 +1107,8 @@ static BOOL WINAPI start_update_thread_once( INIT_ONCE *once, void *param, void
@@ -1002,6 +1139,8 @@ static BOOL WINAPI start_update_thread_once( INIT_ONCE *once, void *param, void
WARN("Failed to increase module's reference count, error: %lu\n", GetLastError());
steam_overlay_event = CreateEventA(NULL, TRUE, FALSE, "__wine_steamclient_GameOverlayActivated");
@@ -951,7 +979,7 @@ index 1a06300c9de..222748df813 100644
start_event = CreateEventA(NULL, FALSE, FALSE, NULL);
if (!start_event) ERR("failed to create start event, error %lu\n", GetLastError());
@@ -1006,6 +1146,7 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved)
@@ -1039,6 +1178,7 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved)
{
case DLL_PROCESS_ATTACH:
xinput_instance = inst;
@@ -959,7 +987,7 @@ index 1a06300c9de..222748df813 100644
persistent_player1 = GetEnvironmentVariableA("PROTON_XINPUT_PERSISTENT_PLAYER1", value, sizeof(value)) == 1 &&
value[0] == '1';
if (persistent_player1) TRACE("Keeping XInput player 1 connected across hotplug.\n");
@@ -1049,7 +1190,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputSetState(DWORD index, XINPUT_VIBRATION *vib
@@ -1082,7 +1222,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputSetState(DWORD index, XINPUT_VIBRATION *vib
EnterCriticalSection(&xinput_cs);
if (!controllers[index].device) update_controller_list();
@@ -968,8 +996,8 @@ index 1a06300c9de..222748df813 100644
ret = is_persistent_player1(index) ? ERROR_SUCCESS : ERROR_DEVICE_NOT_CONNECTED;
else if (WaitForSingleObject(steam_overlay_event, 0) == WAIT_OBJECT_0)
ret = ERROR_SUCCESS;
@@ -1079,7 +1220,7 @@ static DWORD xinput_get_state(DWORD index, XINPUT_STATE *state)
if (sgi && (!strcmp(sgi, "298110") || !strcmp(sgi, "292030"))) goto done;
@@ -1109,7 +1249,7 @@ static DWORD xinput_get_state(DWORD index, XINPUT_STATE *state)
if (ret == ERROR_SUCCESS) goto done;
EnterCriticalSection(&xinput_cs);
- update_controller_list();
@@ -977,7 +1005,7 @@ index 1a06300c9de..222748df813 100644
ret = get_current_state(index, state) ? ERROR_SUCCESS : ERROR_DEVICE_NOT_CONNECTED;
LeaveCriticalSection(&xinput_cs);
@@ -1357,7 +1498,7 @@ DWORD WINAPI __wine_XInputGetSonyProductId(DWORD index, WORD *product_id)
@@ -1387,7 +1527,7 @@ DWORD WINAPI __wine_XInputGetSonyProductId(DWORD index, WORD *product_id)
EnterCriticalSection(&xinput_cs);
if (!controllers[index].device) update_controller_list();
@@ -986,7 +1014,7 @@ index 1a06300c9de..222748df813 100644
ret = ERROR_DEVICE_NOT_CONNECTED;
else
*product_id = controllers[index].hid.sony_product_id;
@@ -1379,7 +1520,7 @@ DWORD WINAPI __wine_XInputGetDeviceVidPid(DWORD index, WORD *vendor_id, WORD *pr
@@ -1409,7 +1549,7 @@ DWORD WINAPI __wine_XInputGetDeviceVidPid(DWORD index, WORD *vendor_id, WORD *pr
EnterCriticalSection(&xinput_cs);
if (!controllers[index].device) update_controller_list();
@@ -995,7 +1023,7 @@ index 1a06300c9de..222748df813 100644
ret = ERROR_DEVICE_NOT_CONNECTED;
else if (swscanf(controllers[index].device_path, L"\\\\?\\HID#VID_%04x&PID_%04x", &vid, &pid) != 2 &&
swscanf(controllers[index].device_path, L"\\\\?\\hid#vid_%04x&pid_%04x", &vid, &pid) != 2)
@@ -1404,7 +1545,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetDSoundAudioDeviceGuids(DWORD index, GUID
@@ -1434,7 +1574,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetDSoundAudioDeviceGuids(DWORD index, GUID
if (index >= XUSER_MAX_COUNT || !render_guid || !capture_guid) return ERROR_BAD_ARGUMENTS;
EnterCriticalSection(&xinput_cs);
@@ -1004,7 +1032,7 @@ index 1a06300c9de..222748df813 100644
LeaveCriticalSection(&xinput_cs);
return ret;
@@ -1420,7 +1561,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetBatteryInformation(DWORD index, BYTE typ
@@ -1450,7 +1590,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetBatteryInformation(DWORD index, BYTE typ
if (index >= XUSER_MAX_COUNT) return ERROR_BAD_ARGUMENTS;
EnterCriticalSection(&xinput_cs);
@@ -1013,7 +1041,7 @@ index 1a06300c9de..222748df813 100644
LeaveCriticalSection(&xinput_cs);
return ret;
@@ -1450,7 +1591,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetCapabilitiesEx(DWORD unk, DWORD index, D
@@ -1480,7 +1620,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetCapabilitiesEx(DWORD unk, DWORD index, D
attr.ProductID = 0x028e;
attr.VersionNumber = 0x0114;
}
@@ -1023,7 +1051,7 @@ index 1a06300c9de..222748df813 100644
if (ret == ERROR_SUCCESS)
diff --git a/include/wine/hid.h b/include/wine/hid.h
index cb31bd6e5a5..d20c6ce5068 100644
index cb31bd6e5a5bf838fd00ea9695e2b3eb72e54347..d20c6ce506846c8f4d2a3b8d05d5224fea333bf2 100644
--- a/include/wine/hid.h
+++ b/include/wine/hid.h
@@ -242,5 +242,13 @@ struct hid_preparsed_data
@@ -25,7 +25,7 @@ it does not publish or claim the existence of a speaker endpoint. Keep the
normal native HID/speaker ordering otherwise unchanged.
---
diff --git a/dlls/sechost/service.c b/dlls/sechost/service.c
index 320a28d2..5d43ef98 100644
index 320a28d2a910b2c8e5bdd109cb2cc3a5e55f10ce..5d43ef988a3f5fa6f2e9e20575f76460148fdb4b 100644
--- a/dlls/sechost/service.c
+++ b/dlls/sechost/service.c
@@ -2262,6 +2262,10 @@ static DWORD WINAPI pending_sony_hid_proc( void *arg )
@@ -40,7 +40,7 @@ index 320a28d2..5d43ef98 100644
dispatch = !pending->cancelled;
ReleaseSRWLockExclusive( &pending_sony_hid_lock );
diff --git a/dlls/xinput1_1/Makefile.in b/dlls/xinput1_1/Makefile.in
index 770937e7..98438fb1 100644
index 770937e76129fc8e948cd4664bab48397f2ee3a7..98438fb11f229b49d73dd1c5a70a6b2ff22612a3 100644
--- a/dlls/xinput1_1/Makefile.in
+++ b/dlls/xinput1_1/Makefile.in
@@ -1,6 +1,6 @@
@@ -52,7 +52,7 @@ index 770937e7..98438fb1 100644
SOURCES = \
diff --git a/dlls/xinput1_2/Makefile.in b/dlls/xinput1_2/Makefile.in
index a8c71b40..6804d8e8 100644
index a8c71b40ca896c6cd7f021e465e9a6e1eb51dc05..6804d8e8507284267c9d35480111afd78261fdbe 100644
--- a/dlls/xinput1_2/Makefile.in
+++ b/dlls/xinput1_2/Makefile.in
@@ -1,6 +1,6 @@
@@ -64,7 +64,7 @@ index a8c71b40..6804d8e8 100644
SOURCES = \
diff --git a/dlls/xinput1_3/Makefile.in b/dlls/xinput1_3/Makefile.in
index d2ffaf29..eb7ec111 100644
index d2ffaf297ec966d5b6b5dc7e10d345419d9853bc..eb7ec11102a8d62a8ac14f9ce84ad0cff6219875 100644
--- a/dlls/xinput1_3/Makefile.in
+++ b/dlls/xinput1_3/Makefile.in
@@ -1,7 +1,7 @@
@@ -77,7 +77,7 @@ index d2ffaf29..eb7ec111 100644
SOURCES = \
main.c \
diff --git a/dlls/xinput1_3/main.c b/dlls/xinput1_3/main.c
index 49facd58..72d9b520 100644
index 09ad9fcf8d4e8ae6e7471f48f4365a2f2e07f3f1..4e0832fb5fc31f566c857ba5fba5d96b7d6da8d4 100644
--- a/dlls/xinput1_3/main.c
+++ b/dlls/xinput1_3/main.c
@@ -36,7 +36,7 @@
@@ -89,7 +89,7 @@ index 49facd58..72d9b520 100644
#include "devpkey.h"
#include "hidusage.h"
#include "winioctl.h"
@@ -770,51 +770,67 @@ static void try_remove_device(const WCHAR *device_path)
@@ -773,14 +773,36 @@ static void try_remove_device(const WCHAR *device_path)
controller_destroy(&controllers[i], FALSE);
}
@@ -131,6 +131,9 @@ index 49facd58..72d9b520 100644
BOOL native_present = FALSE;
EnterCriticalSection(&xinput_cs);
@@ -813,40 +835,34 @@ static void update_controller_list(void)
if (RegNotifyChangeKeyValue(deviface_key, TRUE, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET, key_changed_event, TRUE))
ERR("RegNotifyChangeKeyValue failed.\n");
- set = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
- detail->cbSize = sizeof(*detail);
@@ -156,6 +159,7 @@ index 49facd58..72d9b520 100644
+ free(paths);
InterlockedExchange(&steam_virtual_present, native_present);
automatic_sony:
if (auto_sony_xinput && !steam_virtual_present)
{
HidD_GetHidGuid(&guid);
@@ -180,7 +184,7 @@ index 49facd58..72d9b520 100644
}
diff --git a/dlls/xinput1_4/Makefile.in b/dlls/xinput1_4/Makefile.in
index 482f75d0..e282d450 100644
index 482f75d0f7e460d4a03ad3eccc9a160b1a3f8c8c..e282d450b6633a336b097efc01e40865c2ec34e0 100644
--- a/dlls/xinput1_4/Makefile.in
+++ b/dlls/xinput1_4/Makefile.in
@@ -1,6 +1,6 @@
@@ -24,10 +24,10 @@ controller the update thread has not processed yet.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/777
---
diff --git a/dlls/xinput1_3/main.c b/dlls/xinput1_3/main.c
index 558f05b43c4..3250eeb1909 100644
index 335fa83c92b8703932a3ce23a95058b625f10eab..438cf405d552f80d34a05460cdcb597646459e17 100644
--- a/dlls/xinput1_3/main.c
+++ b/dlls/xinput1_3/main.c
@@ -796,11 +796,14 @@ static WCHAR *get_device_interfaces(GUID *guid)
@@ -799,11 +799,14 @@ static WCHAR *get_device_interfaces(GUID *guid)
return NULL;
}
@@ -43,8 +43,8 @@ index 558f05b43c4..3250eeb1909 100644
EnterCriticalSection(&xinput_cs);
@@ -820,6 +823,13 @@ static void update_controller_list(void)
@@ -852,6 +855,13 @@ static void update_controller_list(void)
automatic_sony:
if (auto_sony_xinput && !steam_virtual_present)
{
+ /* Games polling empty slots would walk the whole HID class on every
@@ -57,7 +57,7 @@ index 558f05b43c4..3250eeb1909 100644
HidD_GetHidGuid(&guid);
if (!(paths = get_device_interfaces(&guid))) goto done;
for (path = paths; *path; path += wcslen(path) + 1)
@@ -1030,7 +1040,7 @@ static LRESULT CALLBACK xinput_devnotify_wndproc(HWND hwnd, UINT msg, WPARAM wpa
@@ -1062,7 +1072,7 @@ static LRESULT CALLBACK xinput_devnotify_wndproc(HWND hwnd, UINT msg, WPARAM wpa
if (wparam == DBT_DEVICEREMOVECOMPLETE)
{
try_remove_device(iface->dbcc_name);
@@ -66,7 +66,7 @@ index 558f05b43c4..3250eeb1909 100644
}
LeaveCriticalSection(&xinput_cs);
}
@@ -1072,13 +1082,13 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
@@ -1104,13 +1114,13 @@ static DWORD WINAPI hid_update_thread_proc(void *param)
hid_notif = RegisterDeviceNotificationW(hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE);
}
@@ -82,7 +82,7 @@ index 558f05b43c4..3250eeb1909 100644
EnterCriticalSection(&xinput_cs);
if (ret < count - 1 && devices[ret]->device) read_controller_state(devices[ret]);
@@ -1200,7 +1210,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputSetState(DWORD index, XINPUT_VIBRATION *vib
@@ -1232,7 +1242,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputSetState(DWORD index, XINPUT_VIBRATION *vib
if (index >= XUSER_MAX_COUNT) return ERROR_BAD_ARGUMENTS;
EnterCriticalSection(&xinput_cs);
@@ -91,8 +91,8 @@ index 558f05b43c4..3250eeb1909 100644
if (!controller_available(&controllers[index]))
ret = is_persistent_player1(index) ? ERROR_SUCCESS : ERROR_DEVICE_NOT_CONNECTED;
else if (WaitForSingleObject(steam_overlay_event, 0) == WAIT_OBJECT_0)
@@ -1231,7 +1241,7 @@ static DWORD xinput_get_state(DWORD index, XINPUT_STATE *state)
if (sgi && (!strcmp(sgi, "298110") || !strcmp(sgi, "292030"))) goto done;
@@ -1260,7 +1270,7 @@ static DWORD xinput_get_state(DWORD index, XINPUT_STATE *state)
if (ret == ERROR_SUCCESS) goto done;
EnterCriticalSection(&xinput_cs);
- if (!controllers[index].device) update_controller_list();
@@ -100,7 +100,7 @@ index 558f05b43c4..3250eeb1909 100644
ret = get_current_state(index, state) ? ERROR_SUCCESS : ERROR_DEVICE_NOT_CONNECTED;
LeaveCriticalSection(&xinput_cs);
@@ -1612,7 +1622,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetCapabilitiesEx(DWORD unk, DWORD index, D
@@ -1641,7 +1651,7 @@ DWORD WINAPI DECLSPEC_HOTPATCH XInputGetCapabilitiesEx(DWORD unk, DWORD index, D
if (index >= XUSER_MAX_COUNT) return ERROR_BAD_ARGUMENTS;
EnterCriticalSection(&xinput_cs);
@@ -0,0 +1,141 @@
From: GloriousEggroll <[email protected]>
Date: Mon, 26 Oct 2026 12:00:00 -0600
Subject: [PATCH] ntdll: Only read the kernel clocksource for QPC at start-up.
The user-mode QueryPerformanceCounter path read current_clocksource again
on the first call made more than a second after the last check, through
NtOpenFile/NtReadFile from inside RtlQueryPerformanceCounter. Programs
hook those functions: Reloaded-II's File Emulation Framework, which most
Persona 5 Royal mods depend on, hooks file I/O with managed code. A timer
call from a thread that cannot run managed code then re-enters the .NET
runtime through the hook, and the runtime aborts the process
(RaiseFailFastException, 0x80131506) shortly after the mods load.
Keep the start-up read, which happens while the process is starting and
before such hooks are installed, and drop the periodic one. A TSC that
the kernel demotes after start-up is no longer followed; a TSC read
below the calibrated base still takes the syscall path for that call.
WINE_ENABLE_QPC_TSC=1, which skips both reads, was confirmed by a user to
fix the crash. This change on its own has not been runtime-tested.
Fixes https://github.com/GloriousEggroll/proton-ge-custom/issues/873
---
--- a/dlls/ntdll/time.c
+++ b/dlls/ntdll/time.c
@@ -392,14 +392,16 @@
* keeping them aligned is the firmware's job and some machines are seconds off. Linux tests
* that at boot (check_tsc_sync_source) and falls back to hpet or acpi_pm when it fails, as
* Windows does when its own boot-time check fails. current_clocksource is that verdict, read
- * through the unix namespace at start-up, then on the first call made more than a second
- * after the last check, so a TSC the kernel demotes later is dropped too.
+ * through the unix namespace once, at start-up. It is not read again later: the file read
+ * goes through NtOpenFile/NtReadFile, which programs hook (Reloaded-II's File Emulation
+ * Framework does, with managed code), and a timer call from a thread that cannot run that
+ * code then aborts the process.
*
* Calibration publishes a rate once two consecutive passes agree. Each (counter, TSC) pair is
* bracketed by two rdtscp and discarded unless both report the same processor and lie within
* QPC_TSC_SAMPLE_CYCLES of each other.
*
- * The hot path is rdtscp, two predictable compares and one 64x64->128 multiply, with plain
+ * The hot path is rdtscp, one predictable compare and one 64x64->128 multiply, with plain
* loads only: no locked instruction and no shared write, because an earlier version enforcing
* monotonicity with an interlocked clamp bounced a cache line between the callers and gave no
* gain at all. A TSC read below the calibrated base would wrap the unsigned subtraction into
@@ -411,7 +413,6 @@
#define QPC_TSC_RATE_TOLERANCE 2000 /* two rates agree when within 1/2000 */
#define QPC_TSC_MAX_PASSES 32 /* give up calibrating if they never do */
#define QPC_TSC_MAX_PROBES 64 /* retries while the clocksource is unreadable */
-#define QPC_TSC_CHECK_TICKS TICKSPERSEC /* ask the kernel again once a second */
enum
{
@@ -424,15 +425,12 @@
};
static volatile LONG qpc_tsc_state;
-static volatile LONG qpc_tsc_checking; /* one thread asking the kernel at a time */
-static BOOL qpc_tsc_forced; /* WINE_ENABLE_QPC_TSC: do not follow the kernel clocksource */
/* written before qpc_tsc_state releases them, then constant; qpc_tsc_base_qpc is also read by
* calibrating callers while a pass is being closed, hence the relaxed atomics on it */
static LONGLONG qpc_tsc_base_qpc;
static ULONGLONG qpc_tsc_base_tsc;
static ULONGLONG qpc_tsc_mult; /* TICKSPERSEC * 2^32 / tsc_hz */
-static ULONGLONG qpc_tsc_check_step; /* QPC_TSC_CHECK_TICKS expressed in TSC cycles */
/* calibration state, owned by whoever holds QPC_TSC_PROBING or QPC_TSC_PUBLISHING */
static LONGLONG qpc_tsc_first_qpc; /* the very first sample, for the full-span rate */
@@ -441,10 +439,6 @@
static LONG qpc_tsc_passes;
static LONG qpc_tsc_probes;
-/* written after publication while every caller reads it: relaxed atomics, which on x86-64
- * are plain 64-bit loads and stores, keep the compiler from tearing or caching it */
-static ULONGLONG qpc_tsc_check_start; /* TSC at the last check; the next one is due a step later */
-
struct qpc_tsc_sample
{
LONGLONG qpc;
@@ -549,7 +543,7 @@
return 0;
}
- if ((qpc_tsc_forced = qpc_tsc_env_set( L"WINE_ENABLE_QPC_TSC" )))
+ if (qpc_tsc_env_set( L"WINE_ENABLE_QPC_TSC" ))
{
TRACE_(qpc)( "WINE_ENABLE_QPC_TSC set, not following the kernel clocksource\n" );
return 1;
@@ -624,13 +618,10 @@
delta = span_hz > hz ? span_hz - hz : hz - span_hz;
if (delta * QPC_TSC_RATE_TOLERANCE <= hz) hz = span_hz;
- /* rebase on this sample so the switch is continuous, arm the first check, then
- * publish (release) */
+ /* rebase on this sample so the switch is continuous, then publish (release) */
qpc_tsc_mult = (ULONGLONG)(((unsigned __int128)TICKSPERSEC << 32) / hz);
qpc_tsc_base_tsc = sample->tsc;
- qpc_tsc_check_step = hz * QPC_TSC_CHECK_TICKS / TICKSPERSEC;
__atomic_store_n( &qpc_tsc_base_qpc, sample->qpc, __ATOMIC_RELAXED );
- __atomic_store_n( &qpc_tsc_check_start, sample->tsc, __ATOMIC_RELAXED );
TRACE_(qpc)( "calibrated at %I64u Hz after %ld passes, using the TSC\n",
hz, qpc_tsc_passes + 1 );
InterlockedExchange( (LONG *)&qpc_tsc_state, QPC_TSC_READY );
@@ -650,25 +641,6 @@
InterlockedExchange( (LONG *)&qpc_tsc_state, QPC_TSC_CALIBRATING );
}
-/* Once a second, one caller asks the kernel whether it still uses the TSC. Returns FALSE
- * once it does not and the TSC path has been given up. */
-static BOOL qpc_tsc_check( ULONGLONG tsc )
-{
- BOOL ok = TRUE;
-
- if (InterlockedCompareExchange( (LONG *)&qpc_tsc_checking, 1, 0 )) return TRUE;
- if (__atomic_load_n( &qpc_tsc_state, __ATOMIC_ACQUIRE ) != QPC_TSC_READY) ok = FALSE;
- else if (!qpc_tsc_forced && qpc_tsc_kernel_clocksource() != 1)
- {
- WARN_(qpc)( "the kernel no longer selects the TSC, falling back to the syscall path\n" );
- InterlockedExchange( (LONG *)&qpc_tsc_state, QPC_TSC_DISABLED );
- ok = FALSE;
- }
- else __atomic_store_n( &qpc_tsc_check_start, tsc, __ATOMIC_RELAXED );
- InterlockedExchange( (LONG *)&qpc_tsc_checking, 0 );
- return ok;
-}
-
static BOOL qpc_tsc_counter( LARGE_INTEGER *counter )
{
/* acquire: the values published below must not be loaded before the state that releases
@@ -691,10 +663,6 @@
tsc, qpc_tsc_base_tsc, cpu );
return FALSE;
}
- /* unsigned: a TSC that went backwards wraps this and asks the kernel at once */
- if (tsc - __atomic_load_n( &qpc_tsc_check_start, __ATOMIC_RELAXED ) >= qpc_tsc_check_step &&
- !qpc_tsc_check( tsc ))
- return FALSE;
counter->QuadPart = qpc_tsc_ticks( tsc );
return TRUE;
}
@@ -0,0 +1,72 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] HACK: ntdll: redirect NVIDIA Streamline dlls for the Streamline
upgrade
When WINE_UPSCALER_REPLACE contains "streamline", load sl.*.dll from
c:\windows\system32\umu\streamline\ when the upgraded copy has that file.
Doing this in load_dll() also covers games that import sl.interposer.dll
statically, which the kernelbase LoadLibraryExW upscaler hack can't see.
https://github.com/GloriousEggroll/proton-ge-custom/issues/875
---
--- a/dlls/ntdll/loader.c
+++ b/dlls/ntdll/loader.c
@@ -3851,6 +3851,48 @@
/***********************************************************************
+ * load_dll_streamline_hack
+ *
+ * Redirect NVIDIA Streamline modules (sl.*.dll) to the upgraded copy that
+ * protonfixes installs when WINE_UPSCALER_REPLACE contains "streamline".
+ * This also covers static imports of sl.interposer.dll, and only applies to
+ * files that exist in the upgraded copy.
+ */
+static BOOL load_dll_streamline_hack( const WCHAR *name, WCHAR *override, DWORD size )
+{
+ static const WCHAR dirW[] = L"\\??\\c:\\windows\\system32\\umu\\streamline\\";
+ static INT enabled = -1;
+ FILE_BASIC_INFORMATION info;
+ UNICODE_STRING overrideW;
+ OBJECT_ATTRIBUTES attr;
+ const WCHAR *base, *p;
+ WCHAR envW[64];
+ size_t len;
+
+ if (enabled == -1)
+ enabled = get_env( L"WINE_UPSCALER_REPLACE", envW, sizeof(envW) ) && wcsstr( envW, L"streamline" );
+ if (!enabled) return FALSE;
+
+ base = name;
+ if ((p = wcsrchr( base, '\\' ))) base = p + 1;
+ if ((p = wcsrchr( base, '/' ))) base = p + 1;
+ len = wcslen( base );
+ if (len < 8 || wcsnicmp( base, L"sl.", 3 ) || wcsicmp( base + len - 4, L".dll" )) return FALSE;
+ if (ARRAY_SIZE(dirW) + len > size) return FALSE;
+
+ swprintf( override, size, L"%s%s", dirW, base );
+ RtlInitUnicodeString( &overrideW, override );
+ InitializeObjectAttributes( &attr, &overrideW, OBJ_CASE_INSENSITIVE, 0, NULL );
+ if (NtQueryAttributesFile( &attr, &info ))
+ {
+ override[0] = 0;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+
+/***********************************************************************
* load_dll (internal)
*
* Load a PE style module according to the load order.
@@ -3875,7 +3917,8 @@
if (nts)
{
- if ( (do_override = load_dll_optiscaler_hack(libname, override, ARRAY_SIZE(override))) )
+ if ( (do_override = load_dll_optiscaler_hack(libname, override, ARRAY_SIZE(override))) ||
+ (do_override = load_dll_streamline_hack(libname, override, ARRAY_SIZE(override))) )
FIXME ( "HACK: redirecting %s to %s\n", debugstr_w(libname), debugstr_w(override) );
nts = find_dll_file( load_path, do_override ? override : libname, &nt_name, pwm, &mapping, &image_info, &id,
&redirected, FALSE );
@@ -1,23 +0,0 @@
From 4c420ccec19d0ebe126542ba917d886bd7b375b6 Mon Sep 17 00:00:00 2001
From: NelloKudo <[email protected]>
Date: Tue, 4 Mar 2025 15:43:59 +0000
Subject: [PATCH] winex11: add Muffin (Cinnamon) to Mutter detection
Without this addition, every Mutter-specific workaround will fail to apply to Cinnamon's
compositor Muffin, which is nothing but a Mutter fork.
---
dlls/winex11.drv/window.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c
index c1a9d1c1136..a3723aebe4e 100644
--- a/dlls/winex11.drv/window.c
+++ b/dlls/winex11.drv/window.c
@@ -4161,6 +4161,7 @@ void net_supporting_wm_check_init( struct x11drv_thread_data *data )
char const *sgi = getenv( "SteamGameId" );
if (!strcmp( data->window_manager, "GNOME Shell" )) strcpy( data->window_manager, "Mutter" );
+ if (!strcmp( data->window_manager, "Mutter (Muffin)" )) strcpy( data->window_manager, "Mutter" );
TRACE( "Detected window manager: %s\n", debugstr_a(data->window_manager) );
/* Street Fighter V expects a certain sequence of window resizes
@@ -0,0 +1,39 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] upscalers: add PROTON_STREAMLINE_UPGRADE
Download NVIDIA Streamline into drive_c/windows/system32/umu/streamline/
(the same component OptiScaler uses) and add "streamline" to
WINE_UPSCALER_REPLACE, so Wine loads the upgraded sl.*.dll instead of the
game's copy. Opt-in, and not combined with OptiScaler, which manages its
own Streamline copy.
https://github.com/GloriousEggroll/proton-ge-custom/issues/875
---
--- a/upscalers.py
+++ b/upscalers.py
@@ -758,6 +758,8 @@
ffx3_version: str = get_version(env, 'PROTON_FFX3_UPGRADE', '1.0.1.41314')
ffx4_version: str = get_version(env, 'PROTON_FFX4_UPGRADE', 'default')
+ streamline_version: str = get_version(env, 'PROTON_STREAMLINE_UPGRADE', 'default')
+
optiscaler_version = get_version(env, 'PROTON_USE_OPTISCALER', 'default')
# if optiscaler is enabled, control ffx4_version through fsr4_version unless explicitly set
if 'optiscaler' in compat_config and 'fsr4' in compat_config and 'ffx4' not in compat_config:
@@ -779,6 +781,16 @@
log.info(f'Automatic {name} upgrade enabled.')
upscaler_replace.add(name)
+ # NVIDIA Streamline (sl.*.dll), for games that ship a Streamline version
+ # too old for the driver's DLSS overrides. Wine redirects the game's
+ # sl.*.dll to the upgraded copy.
+ if 'streamline' in compat_config and 'optiscaler' not in compat_config:
+ log.info(f'Setting up streamline version "{streamline_version}".')
+ download_component('streamline', compat_dir, prefix_dir, streamline_version)
+ if check_component('streamline', compat_dir, prefix_dir, streamline_version, ignore_version=True):
+ log.info('Automatic streamline upgrade enabled.')
+ upscaler_replace.add('streamline')
+
if 'optiscaler' in compat_config:
if setup_optiscaler(env, compat_dir, prefix_dir, optiscaler_version):
log.info('OptiScaler enabled.')
@@ -1,308 +0,0 @@
From 655c1834b7b4b238586a91b3deb1e21c1cbac3da Mon Sep 17 00:00:00 2001
From: Stelios Tsampas <[email protected]>
Date: Tue, 23 Jun 2026 12:40:43 +0300
Subject: [PATCH 1/2] upscalers: add optiscaler downloader
Config values are passed through the `PROTON_OPTISCALER_CONFIG` environment
variable. The config is a semicolon (`;`) separated string of individual options.
For each option the template is `{header}.{option}={value}`, i.e to set the following
```
[Upscalers]
Dx11Upscaler=fsr31
Dx12Upscaler=dlss
```
the config should be
```
PROTON_OPTISCALER_CONFIG="Upscalers.Dx11Upscaler=fsr31;Upscalers.Dx12Upscaler=dlss"`
```
To avoid mistakes, config options should already exist in `Optiscaler.ini`.
The changes are to the config are permanent, it has to be either manually edited or
a different config should be applied through the env variable.
Using this will also remove any comments from the `OptiScaler.ini` file.
---
upscalers.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 206 insertions(+), 3 deletions(-)
diff --git a/upscalers.py b/upscalers.py
index ab17ebd..06f551a 100644
--- a/upscalers.py
+++ b/upscalers.py
@@ -1,9 +1,11 @@
"""Download and setup DLLs to upgrade various upscalers"""
+import configparser
import hashlib
import json
import lzma
import os
+import tarfile
import urllib.request
from functools import lru_cache
from urllib.error import HTTPError, URLError
@@ -80,11 +82,16 @@ def __dll_download_exists(url: str) -> bool:
__dlss_section = 'dlss_files'
__xess_section = 'xess_files'
__fsr4_section = 'fsr4_files'
+__opti_section = 'opti_files'
__ffx3_section = 'ffx3_files'
__ffx4_section = 'ffx4_files'
__version_file = 'upscaler_files'
+def __get_opti_files(version: str = 'default') -> tuple[str, dict]:
+ return 'drive_c/windows/system32/umu/', __get_dll_manifest('optiscaler', version)
+
+
def __get_dlss_dlls(version: str = 'default') -> dict:
return {
'drive_c/windows/system32/umu/nvngx_dlss.dll': __get_dll_manifest('dlss', version),
@@ -282,6 +289,67 @@ def check_upscaler(
)
+def check_optiscaler(
+ compat_dir: str,
+ prefix_dir: str,
+ version: str = 'default',
+ *,
+ ignore_version: bool = False,
+) -> bool:
+ """Check for optiscaler files and its version
+
+ version: the version of the optiscaler to download
+ ignore_version: ignore version mismatch but still check if the files are present
+ """
+ try:
+ path, item = __get_opti_files(version)
+ except Exception as e:
+ log.crit('Failed to get "optiscaler" versions from manifest')
+ log.crit(str(e))
+ return False
+
+ try:
+ tracked_items = __get_tracked_items(compat_dir, __opti_section)
+ # test if new files and their attributes exist in the tracking file
+ for dst in [os.path.join(path, f) for f in item['md5_hash']]:
+ _ = tracked_items[dst].get('md5_hash')
+ except Exception as e:
+ log.warn(str(e))
+ return False
+
+ dst_hashes = {}
+ for dst in tracked_items:
+ target = os.path.join(prefix_dir, dst)
+ if not os.path.isfile(target):
+ log.warn(f'Missing file from prefix "{dst}"')
+ return False
+ with open(target, 'rb') as dst_fd:
+ dst_md5 = hashlib.md5(dst_fd.read()).hexdigest().lower()
+ version_md5 = tracked_items[dst]['md5_hash']
+ if version_md5 and dst_md5 != version_md5.lower():
+ log.warn(f'MD5 checksum mismatch between version file and prefix "{dst}"')
+ return False
+ dst_hashes.update({dst: dst_md5})
+
+ if not ignore_version:
+ for name in item['md5_hash']:
+ dst = os.path.join(path, name)
+ target = os.path.join(prefix_dir, dst)
+ if not os.path.isfile(target):
+ log.warn(f'Missing file from prefix "{dst}"')
+ return False
+ if tracked_items[dst]['version'] != item['version']:
+ log.warn(f'Version mismatch between configuration and prefix "{dst}"')
+ return False
+ item_md5 = item['md5_hash'][name]
+ if item_md5 and dst_hashes[dst] != item_md5.lower():
+ log.warn(f'MD5 checksum mismatch between manifest and prefix "{dst}"')
+ return False
+ log.debug(f'Found matching file in prefix "{dst}"')
+
+ return True
+
+
def __download_upscaler_files(
compat_dir: str,
prefix_dir: str,
@@ -408,6 +476,64 @@ def download_upscaler(
log.crit(str(e))
+def download_optiscaler(
+ compat_dir: str, prefix_dir: str, version: str = 'default'
+) -> None:
+ """Check for optiscaler files and their versions
+
+ version: the version of the upscaler dll to download
+ """
+ if check_optiscaler(compat_dir, prefix_dir, version, ignore_version=False):
+ return
+ log.info('Failed to validate "optiscaler" files.')
+
+ try:
+ tracked_items = __get_tracked_items(compat_dir, __opti_section)
+ except Exception:
+ tracked_items = {}
+ pass
+
+ try:
+ path, item = __get_opti_files(version)
+ log.info(f'Updating OptiScaler "{os.path.basename(item["version"])}"')
+
+ url_path = Path(unquote(urlparse(item['download_url']).path))
+ cache_dir = config.path.cache_dir.joinpath('upscalers')
+ cache_dir.mkdir(parents=True, exist_ok=True)
+ cached_file = cache_dir.joinpath(url_path.name)
+ __cached_download(item, cached_file)
+
+ for target in tracked_items:
+ # do not remove .ini files
+ if target.endswith('.ini'):
+ continue
+ local_file = os.path.join(prefix_dir, target)
+ if os.path.exists(local_file):
+ os.unlink(local_file)
+
+ with tarfile.open(cached_file, 'r:xz') as tar_fd:
+ names = tar_fd.getnames()
+ for name in names:
+ local_file = os.path.join(prefix_dir, path, name)
+ if name.endswith('.ini') and os.path.exists(local_file):
+ os.rename(local_file, local_file + '.old' )
+ tar_fd.extract(name, os.path.join(prefix_dir, path))
+
+ version = {}
+ for name in item['md5_hash']:
+ version[os.path.join(path, name)] = {
+ 'version': item['version'],
+ 'md5_hash': item['md5_hash'][name]
+ }
+
+ except Exception as e:
+ log.crit('Failed to download OptiScaler package.')
+ log.crit(str(e))
+ return
+
+ __set_tracked_items(compat_dir, __opti_section, version)
+
+
def setup_upscaler(
name: str,
compat_dir: str,
@@ -420,6 +546,71 @@ def setup_upscaler(
return enabled
+def setup_optiscaler(
+ env: dict,
+ compat_dir: str,
+ prefix_dir: str,
+ version: str,
+) -> bool:
+ log.info(f'Setting up OptiScaler version "{version}".')
+ download_optiscaler(compat_dir, prefix_dir, version)
+ enabled = check_optiscaler(compat_dir, prefix_dir, version, ignore_version=True)
+
+ if enabled and (env_config := env.get('PROTON_OPTISCALER_CONFIG', '')):
+ path, _ = __get_opti_files(version)
+ ini = os.path.join(prefix_dir, path, 'OptiScaler.ini')
+ parser = configparser.ConfigParser()
+ parser.read(ini)
+
+ env_config_parts = env_config.split(';')
+ env_config_parts = (part for part in env_config_parts if part)
+ for part in env_config_parts:
+ try:
+ option_path, value = part.split('=')
+ section, option = option_path.split('.')
+ except Exception as e:
+ log.crit(f'Malformed config "{part}"')
+ log.debug(str(e))
+ continue
+ if section in parser and option in parser[section]:
+ parser[section][option] = value
+ else:
+ log.warn(f'Ignoring unknown option "{f"{section}.{option}"}"')
+
+ with open(ini, 'w') as ini_fd:
+ parser.write(ini_fd)
+
+ return enabled
+
+
+def clean_upscaler(
+ name: str,
+ compat_dir: str,
+ prefix_dir: str,
+) -> bool:
+ try:
+ _, _, section = __get_upscaler_items(name, 'default')
+ except Exception:
+ return False
+
+ log.info(f'Cleaning {name.upper()} files.')
+
+ try:
+ tracked_items = __get_tracked_items(compat_dir, section)
+ except Exception:
+ return False
+
+ __set_tracked_items(compat_dir, section, {})
+
+ for path in tracked_items:
+ file = os.path.join(prefix_dir, path)
+ if os.path.exists(file):
+ log.debug(f'Removing {file}')
+ os.unlink(file)
+
+ return True
+
+
def get_version(env: dict, key: str, fallback: str) -> str:
return env[key] if env.get(key, '0') not in {'0', '1'} else fallback
@@ -436,17 +627,18 @@ def setup_upscalers(
fsr4_version = get_version(env, 'PROTON_FSR4_UPGRADE', 'default')
+ optiscaler_version = get_version(env, 'PROTON_USE_OPTISCALER', 'default')
ffx3_version = get_version(env, 'PROTON_FFX3_UPGRADE', '1.0.1.41314')
ffx4_version = get_version(env, 'PROTON_FFX4_UPGRADE', 'default')
upscaler_replace = set()
upscalers = (
- ('dlss', dlss_version, 'dlss' in compat_config),
- ('xess', xess_version, 'xess' in compat_config),
+ ('dlss', dlss_version, 'dlss' in compat_config or 'optiscaler' in compat_config),
+ ('xess', xess_version, 'xess' in compat_config or 'optiscaler' in compat_config),
# amdxcffx64 4.1.1
('fsr4', fsr4_version, 'fsr4' in compat_config),
# fidelityfx sdk
- ('ffx3', ffx3_version, 'ffx3' in compat_config),
+ ('ffx3', ffx3_version, 'ffx3' in compat_config or 'optiscaler' in compat_config),
('ffx4', ffx4_version, 'ffx4' in compat_config),
)
for upscaler in upscalers:
@@ -455,6 +646,17 @@ def setup_upscalers(
log.info(f'Automatic {name.upper()} upgrade enabled.')
upscaler_replace.add(name)
+ if 'optiscaler' in compat_config:
+ if setup_optiscaler(env, compat_dir, prefix_dir, optiscaler_version):
+ log.info('OptiScaler enabled.')
+ if 'fsr4' in compat_config:
+ if fsr4_version in {'4.0.0', '4.0.1', 'default'}:
+ setup_upscaler('ffx3', compat_dir, prefix_dir, ffx3_version)
+ else:
+ setup_upscaler('ffx4', compat_dir, prefix_dir, fsr4_version)
+ env['WINE_OPTISCALER_NAME'] = env.get('PROTON_OPTISCALER_NAME', 'dxgi.dll')
+ upscaler_replace.clear()
+
if 'fsr4' in upscaler_replace:
pass
--
2.55.0
@@ -0,0 +1,30 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] debug: show the Proton build and the prefix version separately
The proton script now writes Valve's prefix version (e.g. 11.0-100)
instead of the GE build name, so "Proton Version" would no longer say
which build is running. Print the build name from proton_build_name()
when the script provides it, and the prefix version on its own line.
---
--- a/debug.py
+++ b/debug.py
@@ -3,6 +3,7 @@
import os
import sys
+import __main__
from __main__ import CURRENT_PREFIX_VERSION, g_proton
from .logger import log
@@ -29,6 +30,11 @@
log.debug(line)
log.debug('Proton Version:')
+ build_name = getattr(__main__, 'proton_build_name', None)
+ log.debug(build_name() if build_name else CURRENT_PREFIX_VERSION)
+ log.debug(line)
+
+ log.debug('Prefix Version:')
log.debug(CURRENT_PREFIX_VERSION)
log.debug(line)
@@ -1,47 +0,0 @@
From c9a3d5b0d6ca5f932b8e98a2df0047683ef21728 Mon Sep 17 00:00:00 2001
From: Stelios Tsampas <[email protected]>
Date: Thu, 2 Jul 2026 12:38:15 +0300
Subject: [PATCH 2/2] upscalers: control ffx4 version through fsr4 version when
optiscaler is used
---
upscalers.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/upscalers.py b/upscalers.py
index 06f551a..9109c99 100644
--- a/upscalers.py
+++ b/upscalers.py
@@ -627,10 +627,14 @@ def setup_upscalers(
fsr4_version = get_version(env, 'PROTON_FSR4_UPGRADE', 'default')
- optiscaler_version = get_version(env, 'PROTON_USE_OPTISCALER', 'default')
ffx3_version = get_version(env, 'PROTON_FFX3_UPGRADE', '1.0.1.41314')
ffx4_version = get_version(env, 'PROTON_FFX4_UPGRADE', 'default')
+ optiscaler_version = get_version(env, 'PROTON_USE_OPTISCALER', 'default')
+ # if optiscaler is enabled, control ffx4_version through fsr4_version unless explicitly set
+ if 'optiscaler' in compat_config and 'fsr4' in compat_config and 'ffx4' not in compat_config:
+ ffx4_version = fsr4_version
+
upscaler_replace = set()
upscalers = (
('dlss', dlss_version, 'dlss' in compat_config or 'optiscaler' in compat_config),
@@ -650,10 +654,10 @@ def setup_upscalers(
if setup_optiscaler(env, compat_dir, prefix_dir, optiscaler_version):
log.info('OptiScaler enabled.')
if 'fsr4' in compat_config:
- if fsr4_version in {'4.0.0', '4.0.1', 'default'}:
- setup_upscaler('ffx3', compat_dir, prefix_dir, ffx3_version)
+ if ffx4_version in {'4.0.0', '4.0.1'}:
+ clean_upscaler('ffx4', compat_dir, prefix_dir)
else:
- setup_upscaler('ffx4', compat_dir, prefix_dir, fsr4_version)
+ setup_upscaler('ffx4', compat_dir, prefix_dir, ffx4_version)
env['WINE_OPTISCALER_NAME'] = env.get('PROTON_OPTISCALER_NAME', 'dxgi.dll')
upscaler_replace.clear()
--
2.55.0
@@ -0,0 +1,34 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] gamefixes: Kingdoms of Amalur: Re-Reckoning: install
d3dcompiler_43 and d3dcompiler_47
Textures flicker between colors and parts of character models flicker in
and out of existence without the native shader compilers. Installing
d3dcompiler_43 and d3dcompiler_47 fixes it (GOG version via Heroic).
The umu ID covers the GOG, EGS and standalone entries in the database.
https://github.com/Open-Wine-Components/umu-database/issues/159
---
diff --git a/gamefixes-steam/1041720.py b/gamefixes-steam/1041720.py
new file mode 100644
index 0000000..9eefe68
--- /dev/null
+++ b/gamefixes-steam/1041720.py
@@ -0,0 +1,9 @@
+"""Game fix for Kingdoms of Amalur: Re-Reckoning"""
+
+from protonfixes import util
+
+
+def main() -> None:
+ """Textures flicker and parts of character models flicker in and out without the native shader compilers"""
+ util.protontricks('d3dcompiler_43')
+ util.protontricks('d3dcompiler_47')
diff --git a/gamefixes-umu/umu-1041720.py b/gamefixes-umu/umu-1041720.py
new file mode 120000
index 0000000..0d5bfe5
--- /dev/null
+++ b/gamefixes-umu/umu-1041720.py
@@ -0,0 +1 @@
+../gamefixes-steam/1041720.py
\ No newline at end of file
+98 -8
View File
@@ -37,14 +37,13 @@ apply_all_in_dir() {
apply_patch "../patches/dxvk/dxgi-respect-output-hdr-state.patch"
# Query the active output by default instead of requiring an HDR opt-in.
apply_patch "../patches/dxvk/dxgi-auto-detect-hdr.patch"
# StarCraft II: avoid GPU hangs with D3D9 constant buffer streaming (#790).
apply_patch "../patches/dxvk/starcraft2-disable-constant-buffer-streaming.patch"
# StarCraft II: avoid GPU hangs with D3D9 constant buffer streaming on Navi 10 (#790, #845).
apply_patch "../patches/dxvk/starcraft2-disable-constant-buffer-streaming-on-navi10.patch"
popd
pushd d7vk
git reset --hard HEAD
git clean -xdf
apply_patch "../patches/d7vk/d7vk-allow-avx-with-clang.patch"
popd
pushd openvr
@@ -69,7 +68,7 @@ apply_all_in_dir() {
pushd protonfixes
git reset --hard HEAD
git clean -xdf
echo "PROTONFIXES: add optiscaler support"
echo "PROTONFIXES: apply GE protonfixes patches"
apply_all_in_dir "../patches/protonfixes/"
popd
@@ -99,6 +98,7 @@ apply_all_in_dir() {
lsteamclient/Makefile.in \
lsteamclient/gen_wrapper.py \
lsteamclient/steam_input_manual.c \
lsteamclient/steam_matchmaking_manual.c \
lsteamclient/steamclient_main.c \
lsteamclient/steamclient_private.h \
lsteamclient/unix_private.h \
@@ -266,7 +266,15 @@ apply_all_in_dir() {
apply_all_in_dir "../patches/wine-hotfixes/wine-staging/winex11-Window_Style/"
echo "WINE: -STAGING- wininet-Cleanup manually applied"
apply_all_in_dir "../wine-staging/patches/wininet-Cleanup/"
# Skip 0005: it makes HTTP_ADDREQ_FLAG_REPLACE fail for headers not yet
# present, while Windows 10 adds them. TLOU Part II adds Content-Type and
# Authorization that way, so its leaderboard requests were sent without
# them and rejected (403).
# https://github.com/GloriousEggroll/proton-ge-custom/issues/463
apply_patch "../wine-staging/patches/wininet-Cleanup/0001-wininet-tests-Add-more-tests-for-cookies.patch"
apply_patch "../wine-staging/patches/wininet-Cleanup/0002-wininet-tests-Test-auth-credential-reusage-with-host.patch"
apply_patch "../wine-staging/patches/wininet-Cleanup/0003-wininet-tests-Check-cookie-behaviour-when-overriding.patch"
apply_patch "../wine-staging/patches/wininet-Cleanup/0004-wininet-Strip-filename-if-no-path-is-set-in-cookie.patch"
echo "WINE: -STAGING- Staging manually applied"
apply_all_in_dir "../wine-staging/patches/Staging/"
@@ -309,6 +317,7 @@ apply_all_in_dir() {
echo "WINE: -GAME FIXES- add TBH: Task Bar Hero fixes"
apply_patch "../patches/game-patches/layered-overlay-wine.patch"
apply_patch "../patches/game-patches/layered-overlay-direct-present.patch"
# multi-process-launcher-x11-fallback.patch is intentionally disabled.
# Wine-Wayland now renders cross-process launcher windows directly.
@@ -432,6 +441,9 @@ apply_all_in_dir() {
echo "WINE: -HOTFIX- Reconcile unsupported minimization on xwayland-satellite"
apply_patch "../patches/wine-hotfixes/pending/winex11-handle-unsupported-minimization-on-satellite.patch"
echo "WINE: -HOTFIX- winex11: don't wait for the pointer grab with a sent message pending (Arknights: Endfield web view freezes the desktop)"
apply_patch "../patches/wine-hotfixes/pending/winex11-bound-pointer-grab-wait.patch"
echo "WINE: -HOTFIX- Do not paint off-desktop minimized frames into retained host windows"
apply_patch "../patches/wine-hotfixes/pending/server-clip-off-desktop-minimized-window-paint.patch"
@@ -454,6 +466,11 @@ apply_all_in_dir() {
echo "WINE: -HOTFIX- Reserve top-down space for large-address-aware WoW64 applications"
apply_patch "../patches/wine-hotfixes/pending/ntdll-reserve-top-down-space-for-large-address-aware-wow64.patch"
# The NVIDIA driver maps 4G + 2M at the first free address from 0x200000000 for the Vulkan extensions DLSS needs;
# bottom-up allocation (ntdll-ForceBottomUpAlloc) can fill that range (DLSS in Star Citizen, #460).
echo "WINE: -HOTFIX- Keep the address range the NVIDIA driver reserves for DLSS free"
apply_patch "../patches/wine-hotfixes/pending/ntdll-keep-nvidia-dlss-address-range-free.patch"
echo "WINE: -HOTFIX- Remove redundant packed-code split locks"
apply_patch "../patches/wine-hotfixes/pending/ntdll-remove-redundant-packed-split-lock.patch"
@@ -542,17 +559,22 @@ apply_all_in_dir() {
echo "WINE: --CUSTOM-- add WINE_HOSTBLOCK envvar to allow working around some problematic anticheats (notably eac)"
apply_patch "../patches/proton/wine_host_block_envvar.patch"
echo "WINE: mutter -> cinnamon detection patch for winex11"
apply_patch "../patches/proton/winex11-mutter-cinnamon.patch"
echo "WINE: add optiscaler patch"
apply_patch "../patches/proton/0001-HACK-kernelbase-allow-overriding-dlls-for-DLSS-XeSS-.patch"
apply_patch "../patches/proton/0002-HACK-ntdll-add-optiscaler-inection-hack.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/875
echo "WINE: redirect NVIDIA Streamline dlls for PROTON_STREAMLINE_UPGRADE"
apply_patch "../patches/proton/0003-HACK-ntdll-redirect-streamline-dlls-for-upgrade.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/pull/759
echo "WINE: -PERF- read QueryPerformanceCounter from the TSC in user mode (DCS World: 39 -> 62 FPS)"
apply_patch "../patches/proton/0001-ntdll-Read-QueryPerformanceCounter-from-the-TSC-in-us.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/873
echo "WINE: only check the kernel clocksource for QPC at start-up (P5R Reloaded-II mods crash)"
apply_patch "../patches/proton/0002-ntdll-Only-read-the-kernel-clocksource-for-QPC-at-start-up.patch"
echo "WINE: implement IOCTL_SERIAL_GET_DTRRTS (Qt serial device tools, e.g. MOZA Cockpit)"
apply_patch "../patches/proton/0001-ntdll-Implement-IOCTL_SERIAL_GET_DTRRTS-for-serial-dev.patch"
@@ -575,20 +597,88 @@ apply_all_in_dir() {
echo "WINE: -HOTFIX- ntdll: skip needless wake in RtlReleaseSRWLockShared (ValveSoftware/wine#367)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-srwlock-skip-needless-shared-release-wake.patch"
# https://bugs.winehq.org/show_bug.cgi?id=60589
# https://gitlab.winehq.org/wine/wine/-/merge_requests/12472
echo "WINE: -HOTFIX- ntdll: reuse free blocks of partially used LFH groups (EVE Online heap growth)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-heap-reuse-free-blocks-of-partially-used-lfh-groups.patch"
# https://github.com/ValveSoftware/Proton/issues/8072
# https://bugs.winehq.org/show_bug.cgi?id=60523
echo "WINE: -GAME FIXES- ntdll: don't leave a stale alert after a raced RtlWaitOnAddress() timeout (Space Marine 2 mid-mission crash)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-take-in-flight-alert-when-wait-on-address-times-out.patch"
apply_patch "../patches/wine-hotfixes/pending/ntdll-tests-wait-on-address-wake-racing-timeout.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/870
echo "WINE: -GAME FIXES- ntdll: run NCSOFT PURPLE's VMProtect-packed purple-agent.exe in new WoW64 mode (Initialization Error 1)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-run-purple-agent-in-new-wow64-mode.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/870
echo "WINE: -GAME FIXES- cmd: make CHOICE /T time out on console input (NCSOFT PURPLE post-update restart hang)"
apply_patch "../patches/wine-hotfixes/pending/cmd-make-choice-timeout-work-on-console-input.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/837
echo "WINE: -GAME FIXES- win32u: release swapchain images presented to hidden windows instead of blocking the host present (RuneScape Vulkan on winewayland)"
apply_patch "../patches/wine-hotfixes/pending/win32u-release-images-presented-to-hidden-windows.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/837
echo "WINE: -GAME FIXES- win32u: report B8G8R8A8_UNORM as the first surface format like Windows drivers (RuneScape washed-out colors)"
apply_patch "../patches/wine-hotfixes/pending/win32u-report-b8g8r8a8-unorm-surface-format-first.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/898
# https://github.com/GloriousEggroll/proton-ge-custom/issues/916
echo "WINE: -HDR- win32u: prefer the native HDR10/scRGB colorspace when the host supports it (oversaturated MangoHud/Steam overlay in HDR on KWin 6.8)"
apply_patch "../patches/wine-hotfixes/pending/win32u-prefer-native-hdr-colorspace-for-host-layers.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/798
echo "WINE: -WAYLAND- winewayland: fix win_data locking in the IME text input paths (freeze when using Fcitx5 with WAYLANDDRV_IME=1)"
apply_patch "../patches/wine-hotfixes/pending/winewayland-fix-win-data-locking-in-ime-text-input.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/908
echo "WINE: -WAYLAND- winewayland: don't take the USER lock under win_data from client surface callbacks, post WM_CANCELMODE on keyboard leave (Galaxy Idle Clicker freeze and exit hang)"
apply_patch "../patches/wine-hotfixes/pending/winewayland-avoid-user-lock-under-win-data-in-client-surface-callbacks.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/852
echo "WINE: -GAME FIXES- winewayland: advertise the OpenVR/OpenXR Vulkan device pseudo-extensions like winex11 (DXVK/vkd3d device creation fails on Wayland with an OpenXR runtime installed)"
apply_patch "../patches/wine-hotfixes/pending/winewayland-advertise-openvr-openxr-device-extensions.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/919
echo "WINE: -WAYLAND- winewayland: animate animated cursors (game cursors static on Wine-Wayland, The Sims 3)"
apply_patch "../patches/wine-hotfixes/pending/winewayland-animate-animated-cursors.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/899
echo "WINE: -GAME FIXES- rtworkq: don't hold queues_section while shutting down the system queues (MFShutdown deadlock, Kingdom Hearts 358/2 Days port hangs after the first cutscene)"
apply_patch "../patches/wine-hotfixes/pending/rtworkq-dont-hold-queues-section-during-system-queue-shutdown.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/521
echo "WINE: -GAME FIXES- dsound: keep the high quality resampler for any number of buffers (BGM crackles after voice lines in Liarsoft codeX RScript games)"
apply_patch "../patches/wine-hotfixes/pending/dsound-keep-hq-resampler-for-any-buffer-count.patch"
# https://github.com/GloriousEggroll/proton-ge-custom/issues/881
echo "WINE: -HOTFIX- win32u: defer destroying NVIDIA Wayland host Vulkan instances while another is alive (NVIDIA driver clears its global libwayland table, EA app crash on winewayland)"
apply_patch "../patches/wine-hotfixes/pending/win32u-defer-nvidia-wayland-host-instance-destroy.patch"
# https://bugs.winehq.org/show_bug.cgi?id=60417
# https://bugs.winehq.org/show_bug.cgi?id=60524
echo "WINE: -PENDING- server: abandon an ntsync mutex whose handle was closed while it was owned or in use"
apply_patch "../patches/wine-hotfixes/pending/server-keep-owned-ntsync-mutex-until-owner-releases-or-dies.patch"
apply_patch "../patches/wine-hotfixes/pending/server-keep-in-process-mutex-closed-while-in-use.patch"
apply_patch "../patches/wine-hotfixes/pending/kernel32-tests-abandon-mutex-acquired-after-close.patch"
# Bluetooth LE GATT and WinRT BLE (Zwift, Rouvy trainers). winebth.sys stays disabled
# unless PROTON_ENABLE_WINEBTH=1. https://github.com/GloriousEggroll/proton-ge-custom/issues/814
# https://github.com/evanjt/wine
echo "WINE: -HOTFIX- Add Bluetooth LE GATT and WinRT BLE support"
apply_all_in_dir "../patches/wine-hotfixes/bluetooth-le/"
# https://github.com/jegglest/proton-je/blob/je-11.0/docs-je/killed-thread-stacks.md
echo "WINE: -PENDING- ntdll: free the TEB and stacks of a thread killed by another thread (Helldivers 2 memory growth)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-free-teb-and-stacks-of-killed-threads.patch"
apply_patch "../patches/wine-hotfixes/pending/kernel32-tests-killed-thread-stack-freed.patch"
# https://github.com/ValveSoftware/Proton/issues/7486#issuecomment-6594771550
echo "WINE: -PENDING- ntdll: let a thread killed during its startup exit instead of hanging (Helldivers 2 thread growth)"
apply_patch "../patches/wine-hotfixes/pending/ntdll-dont-keep-reply-pipe-open-during-thread-init.patch"
echo "WINE: RUN AUTOCONF TOOLS/MAKE_REQUESTS"
autoreconf -f
./tools/make_requests
@@ -0,0 +1,19 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] vkd3d: Print the version string of dirty-tree builds.
The INFO build line only shows the build hash, so a modified build looks
like upstream apart from the '+'. Also print vkd3d_version, which GE-Proton
tags with "-GE-Proton(dirty)" when the tree carries patches, so bug reports
show that the build is not upstream vkd3d-proton.
---
--- a/libs/vkd3d/device.c
+++ b/libs/vkd3d/device.c
@@ -696,7 +696,7 @@
/* Builds from dirty trees generate vkd3d_version ending with a '+'. */
dirty_tree_build = sizeof(vkd3d_version) >= 2 && vkd3d_version[sizeof(vkd3d_version) - 2] == '+';
if (dirty_tree_build)
- INFO("vkd3d-proton - build: %015"PRIx64"+.\n", vkd3d_build >> 4);
+ INFO("vkd3d-proton - build: %015"PRIx64"+ (%s).\n", vkd3d_build >> 4, vkd3d_version);
else
INFO("vkd3d-proton - build: %015"PRIx64".\n", vkd3d_build);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
From 501a6bb947f8720d3d503c3355cacce080b82b22 Mon Sep 17 00:00:00 2001
From: Adam Lesperance <[email protected]>
Date: Mon, 19 Oct 2026 18:56:40 -0500
Subject: [PATCH] ntdll: Don't switch %fs to pthread_teb for system threads on
x86_64.
System threads created by PsCreateSystemThread() never run
signal_init_thread()/call_init_thunk(), so amd64_thread_data->pthread_teb
is never initialized and stays NULL. In WoW64 processes (fs32_sel != 0)
init_handler() then loads %fs with 0, and the first libc TLS access in the
handler (NtCurrentTeb() -> pthread_getspecific()) faults at %fs:0x10.
This makes every 32-bit process with winewayland's event dispatcher
system thread crash with SIGSEGV on exit, when wineserver sends SIGQUIT
and quit_handler() runs on that thread. SIGUSR1 (usr1_handler) is
affected the same way.
System threads never leave the Unix side, so %fs still holds the libc
TLS base and there is nothing to restore. Upstream avoids this because
init_handler() returns early for threads without amd64 thread data; this
is the equivalent for the 11.0 backport, matching what 0031 does for gs
on i386.
---
dlls/ntdll/unix/signal_x86_64.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/dlls/ntdll/unix/signal_x86_64.c b/dlls/ntdll/unix/signal_x86_64.c
index cd6ba849cc6..8482cfe470f 100644
--- a/dlls/ntdll/unix/signal_x86_64.c
+++ b/dlls/ntdll/unix/signal_x86_64.c
@@ -928,7 +928,10 @@ static inline ucontext_t *init_handler( void *sigcontext )
{
struct amd64_thread_data *thread_data = (struct amd64_thread_data *)&get_current_teb()->GdiTebBatch;
thread_data->syscall_dispatch = 0; /* SYSCALL_DISPATCH_FILTER_ALLOW */
- if (fs32_sel) arch_prctl( ARCH_SET_FS, thread_data->pthread_teb );
+ /* system threads never run PE code, so %fs still holds the libc TLS base
+ * and pthread_teb is never initialized for them */
+ if (fs32_sel && !((struct ntdll_thread_data *)thread_data)->system_thread)
+ arch_prctl( ARCH_SET_FS, thread_data->pthread_teb );
}
#elif defined __APPLE__
{
--
2.56.0
@@ -0,0 +1,105 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] win32u: Advertise the SNI Menu sentinel on GNOME.
0058 stopped advertising Menu=/NO_DBUSMENU because Waybar imports any Menu
path as a dbusmenu and never calls ContextMenu. GNOME Shell's AppIndicator
extension, however, requires the Menu property. It reads it, receives
UnknownProperty, retries, and drops the item after three attempts, so the
tray icon never appears. This affects winewayland applications under GNOME,
since winex11 applications under Mutter use the XEmbed tray instead.
The extension treats /NO_DBUSMENU as no menu and calls ContextMenu, which is
how GE-Proton11-7 worked there. Advertise the sentinel when
XDG_CURRENT_DESKTOP contains GNOME, and keep omitting Menu elsewhere.
Fixes: https://github.com/GloriousEggroll/proton-ge-custom/issues/872
Related: https://github.com/GloriousEggroll/proton-ge-custom/issues/803
References: https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/appIndicator.js
---
diff --git a/dlls/win32u/sni.c b/dlls/win32u/sni.c
--- a/dlls/win32u/sni.c
+++ b/dlls/win32u/sni.c
@@ -483,6 +483,33 @@
p_dbus_message_iter_close_container( iter, &var );
}
+/* GNOME Shell's AppIndicator extension drops items whose Menu property cannot
+ * be read. It treats /NO_DBUSMENU as "no menu" and calls ContextMenu, so
+ * advertise that sentinel there. Other hosts such as Waybar try to import any
+ * Menu path, so keep omitting it elsewhere. */
+static BOOL sni_host_requires_menu(void)
+{
+ static int requires_menu = -1;
+
+ if (requires_menu == -1)
+ {
+ const char *env = getenv( "XDG_CURRENT_DESKTOP" ), *p = env;
+ size_t len;
+
+ requires_menu = 0;
+ while (p && *p)
+ {
+ len = strcspn( p, ":" );
+ if (len == 5 && !strncasecmp( p, "GNOME", 5 )) requires_menu = 1;
+ p += len;
+ if (*p) p++;
+ }
+ TRACE( "XDG_CURRENT_DESKTOP %s, Menu property %s\n", debugstr_a(env),
+ requires_menu ? "advertised" : "omitted" );
+ }
+ return requires_menu;
+}
+
/* write the value of a single named property into iter (already a variant slot) */
static void append_property( DBusMessageIter *iter, const struct sni_icon *icon, const char *name )
{
@@ -516,6 +543,16 @@
}
}
else if (!strcmp( name, "ToolTip" )) append_variant_tooltip( iter, icon );
+ else if (!strcmp( name, "Menu" ))
+ {
+ DBusMessageIter var;
+ const char *path = "/NO_DBUSMENU";
+ if (p_dbus_message_iter_open_container( iter, DBUS_TYPE_VARIANT, "o", &var ))
+ {
+ p_dbus_message_iter_append_basic( &var, DBUS_TYPE_OBJECT_PATH, &path );
+ p_dbus_message_iter_close_container( iter, &var );
+ }
+ }
else append_variant_string( iter, empty );
}
@@ -699,7 +736,7 @@
!strcmp( piface, SNI_ITEM_IFACE ))
{
p_dbus_message_iter_get_basic( &args, &pname );
- if (!strcmp( pname, "Menu" ))
+ if (!strcmp( pname, "Menu" ) && !sni_host_requires_menu())
reply = p_dbus_message_new_error( msg, DBUS_ERROR_UNKNOWN_PROPERTY,
"Tray menus are provided by ContextMenu" );
else if ((reply = p_dbus_message_new_method_return( msg )))
@@ -713,17 +750,18 @@
else if (!strcmp( iface, DBUS_PROPS_IFACE ) && !strcmp( member, "GetAll" ) && icon)
{
/* No D-Bus menu is exported. Omit Menu so hosts call ContextMenu;
- * /NO_DBUSMENU is a KDE convention which other hosts may try to import. */
+ * /NO_DBUSMENU is a KDE convention which other hosts may try to import.
+ * GNOME requires the property, see sni_host_requires_menu. Menu is last. */
static const char *props[] = { "Category", "Id", "Title", "Status", "IconName",
- "IconPixmap", "ToolTip", "ItemIsMenu", "WindowId" };
+ "IconPixmap", "ToolTip", "ItemIsMenu", "WindowId", "Menu" };
+ unsigned int i, count = ARRAY_SIZE(props) - !sni_host_requires_menu();
DBusMessageIter ret, dict;
- unsigned int i;
if ((reply = p_dbus_message_new_method_return( msg )))
{
p_dbus_message_iter_init_append( reply, &ret );
if (p_dbus_message_iter_open_container( &ret, DBUS_TYPE_ARRAY, "{sv}", &dict ))
{
- for (i = 0; i < ARRAY_SIZE(props); i++)
+ for (i = 0; i < count; i++)
{
DBusMessageIter e;
if (!p_dbus_message_iter_open_container( &dict, DBUS_TYPE_DICT_ENTRY, NULL, &e ))
@@ -0,0 +1,52 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] winewayland: Add an explicit override that forces HDR.
0066 stopped DXVK_HDR=1 (set by the legacy PROTON_ENABLE_HDR=1) from
overriding an SDR result for an output, because games such as Saints Row
hang on NVIDIA when HDR is reported on an output with HDR disabled
(#541). Output HDR state now comes only from the compositor's
color-management luminances: HDR is reported when the output's maximum
luminance is above its reference (SDR white) luminance.
Two setups need to override that result:
- KWin with SDR brightness set to the monitor's peak reports
max_lum == ref_lum, which is SDR by that definition, although Plasma's
HDR toggle is on (#838). KWin developers consider this correct, since
there is no HDR headroom, and asked us not to read KWin's private
kde-output-device-v2 state instead, so the patch doing that was
dropped.
- Compositors that switch an output to HDR when a fullscreen client
presents HDR content (e.g. Hyprland's render:cm_auto_hdr) never switch,
since the game sees an SDR output and never requests HDR.
WAYLANDDRV_FORCE_HDR=1, set by Proton for PROTON_FORCE_HDR=1, reports
every output as HDR-enabled. DXVK_NO_HDR=1 and DXVK_HDR=0 still take
precedence. It is a separate opt-in rather than a revival of the
PROTON_ENABLE_HDR behaviour, so existing launch options keep the #541 fix.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/838
Runtime validation needed after rebuilding:
- KWin, HDR on, SDR brightness at the peak: HDR offered with the override,
SDR without it.
- Hyprland with render:cm_auto_hdr and an SDR desktop: monitor switches
to HDR for a fullscreen HDR game with the override.
- DXVK_NO_HDR=1 still disables HDR with the override set.
---
diff --git a/dlls/winewayland.drv/display.c b/dlls/winewayland.drv/display.c
--- a/dlls/winewayland.drv/display.c
+++ b/dlls/winewayland.drv/display.c
@@ -382,7 +382,10 @@
/* We don't have a direct way to get the work area in Wayland. */
monitor.rc_work = monitor.rc_monitor;
monitor.hdr_enabled = output->supports_hdr;
- /* An HDR request cannot enable HDR on an SDR compositor output. */
+ /* An HDR request cannot enable HDR on an SDR compositor output, unless
+ * the user explicitly forces it (PROTON_FORCE_HDR=1). */
+ if ((env = getenv("WAYLANDDRV_FORCE_HDR")) && *env == '1')
+ monitor.hdr_enabled = TRUE;
if ((env = getenv("DXVK_NO_HDR")) && *env == '1')
monitor.hdr_enabled = FALSE;
if ((env = getenv("DXVK_HDR")) && *env == '0')
@@ -17,7 +17,7 @@ index 76d829a0bb097f5187a06a84ed2d818bb3388ef1..7e81c70a80218a9ed83a96d57b35a732
dnl *** Check for modules to disable by default
enable_vcruntime140_1=${enable_vcruntime140_1:-x86_64,arm64ec,aarch64}
+enable_wineopenxr=${enable_wineopenxr:-x86_64,arm64ec,aarch64}
+enable_wineopenxr=${enable_wineopenxr:-i386,x86_64,arm64ec,aarch64}
if test -n "$PE_ARCHS"
then
@@ -0,0 +1,67 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Tue, 20 Oct 2026 19:00:00 -0600
Subject: [PATCH] cmd: Make CHOICE /T time out when reading from a console.
WCMD_choice() implemented the timeout with an overlapped ReadFile() on
stdin followed by WaitForSingleObject() on the overlapped event. Console
handles are synchronous, so that ReadFile() blocks until a key arrives,
and the timeout never starts. With a hidden console no key ever arrives,
so `choice /D Y /T 3` waits forever.
NCSOFT PURPLE restarts itself after an update with
`cmd /C choice /C Y /N /D Y /T 3 & START PurpleLauncher.exe ...`, which
never got past the choice and left the launcher closed.
When stdin is a console, wait on the input handle with the remaining
timeout and take the next key-down event with ReadConsoleInputW(), as
Windows' choice does. Pipes and files keep the existing path.
https://github.com/GloriousEggroll/proton-ge-custom/issues/870
---
diff --git a/programs/cmd/builtins.c b/programs/cmd/builtins.c
--- a/programs/cmd/builtins.c
+++ b/programs/cmd/builtins.c
@@ -406,6 +406,42 @@
{
if (opt_timeout == 0)
answer[0] = opt_default;
+ else if (have_console)
+ {
+ /* Console handles are synchronous, so the overlapped read below would block
+ * until a key is pressed and never time out. Wait on the input handle and
+ * read key events instead. */
+ HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
+ ULONGLONG deadline = GetTickCount64() + (ULONGLONG)opt_timeout * 1000;
+ INPUT_RECORD ir;
+ DWORD count, wait;
+
+ answer[0] = 0;
+ while (!answer[0] && return_code == NO_ERROR)
+ {
+ if (opt_timeout == -1) wait = INFINITE;
+ else
+ {
+ ULONGLONG now = GetTickCount64();
+ wait = now >= deadline ? 0 : deadline - now;
+ }
+ switch (WaitForSingleObject(input, wait))
+ {
+ case WAIT_OBJECT_0:
+ if (!ReadConsoleInputW(input, &ir, 1, &count))
+ return_code = ERROR_INVALID_FUNCTION;
+ else if (count && ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown &&
+ ir.Event.KeyEvent.uChar.UnicodeChar)
+ answer[0] = ir.Event.KeyEvent.uChar.UnicodeChar;
+ break;
+ case WAIT_TIMEOUT:
+ answer[0] = opt_default;
+ break;
+ default:
+ return_code = ERROR_INVALID_FUNCTION;
+ }
+ }
+ }
else
{
LARGE_INTEGER li, zeroli = {0};
@@ -0,0 +1,39 @@
From: GloriousEggroll <[email protected]>
Date: Mon, 2 Nov 2026 22:00:00 -0700
Subject: [PATCH] dsound: Keep the high quality resampler for any number of
buffers.
Staging's dsound-Fast_Mixer switches every resampled buffer to a linear
resampler once more than HQBuffersMax (default 4) buffers exist. That
path reads the live buffer memory, while the no-resample and FIR paths
read the committed region between the play and write cursors (Wine
commit 975d0632a19), so audio a game streams into that region can
glitch.
Liarsoft's codeX RScript games play 44.1 kHz buffers and reach 5 of
them during play, and their BGM crackles after voice lines on a 48 kHz
device. Forcing the device to 44.1 kHz avoids resampling and the
crackle, and Proton and upstream Wine, which only have the FIR path,
don't crackle.
Default HQBuffersMax to INT_MAX so the FIR path is always used, as
upstream. The registry value can still lower it.
https://github.com/GloriousEggroll/proton-ge-custom/issues/521
---
--- a/dlls/dsound/dsound_main.c
+++ b/dlls/dsound/dsound_main.c
@@ -81,7 +81,12 @@
/* All default settings, you most likely don't want to touch these, see wiki on UsefulRegistryKeys */
int ds_hel_buflen = 32768 * 2;
-int ds_hq_buffers_max = 4;
+/* Staging's linear resampler is used once more than this many buffers exist. It
+ * reads the live buffer instead of the committed region the other mixing paths
+ * use, so streamed audio can glitch when a game rewrites that region. Keep the
+ * high quality resampler for any number of buffers, like upstream Wine; the
+ * HQBuffersMax registry value can still lower it. */
+int ds_hq_buffers_max = 0x7fffffff;
BOOL ds_eax_enabled = TRUE;
#define IS_OPTION_TRUE(ch) \
@@ -0,0 +1,121 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Tue, 27 Oct 2026 22:22:05 -0700
Subject: [PATCH] kernel32/tests: Test abandoning a mutex acquired after it was
closed.
A wait that was pending when the last handle to an unowned mutex was
closed can still acquire the mutex afterwards, here a wait-all whose
event is set later. If that thread then dies, the mutex must be
abandoned for the next waiter, as it is on Windows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/kernel32/tests/sync.c | 87 ++++++++++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/dlls/kernel32/tests/sync.c b/dlls/kernel32/tests/sync.c
index 1ac1dc0d6de..93fd599f09e 100644
--- a/dlls/kernel32/tests/sync.c
+++ b/dlls/kernel32/tests/sync.c
@@ -405,6 +405,92 @@ static void test_mutex(void)
CloseHandle(params.stop_event);
}
+struct mutex_wait_all_params
+{
+ HANDLE mutex;
+ HANDLE event;
+ HANDLE ready_event;
+ HANDLE done_event;
+ HANDLE stop_event;
+ DWORD ret;
+};
+
+static DWORD WINAPI mutex_wait_all_thread(void *arg)
+{
+ struct mutex_wait_all_params *params = arg;
+ HANDLE objs[2] = {params->mutex, params->event};
+
+ SetEvent(params->ready_event);
+ params->ret = WaitForMultipleObjects(2, objs, TRUE, 5000);
+ SetEvent(params->done_event);
+ WaitForSingleObject(params->stop_event, INFINITE);
+ return 0;
+}
+
+static void test_mutex_wait_all(void)
+{
+ struct mutex_wait_all_params params[2];
+ HANDLE threads[2], mutex, ready, done, stop;
+ DWORD ret;
+ int i;
+
+ /* Nobody owns the mutex when its last handle is closed, but two threads are waiting for it together with an
+ * event each. The first one to get it dies owning it, which must abandon it for the second one. */
+ mutex = CreateMutexA(NULL, FALSE, NULL);
+ ready = CreateEventW(NULL, FALSE, FALSE, NULL);
+ done = CreateEventW(NULL, FALSE, FALSE, NULL);
+ stop = CreateEventW(NULL, TRUE, FALSE, NULL);
+ for (i = 0; i < ARRAY_SIZE(params); i++)
+ {
+ params[i].mutex = mutex;
+ params[i].event = CreateEventW(NULL, FALSE, FALSE, NULL);
+ params[i].ready_event = ready;
+ params[i].done_event = done;
+ params[i].stop_event = stop;
+ params[i].ret = 0xdeadbeef;
+ threads[i] = CreateThread(NULL, 0, mutex_wait_all_thread, &params[i], 0, NULL);
+ ok(!!threads[i], "CreateThread failed, error %lu\n", GetLastError());
+ ret = WaitForSingleObject(ready, 1000);
+ ok(!ret, "got %#lx\n", ret);
+ }
+ ret = WaitForSingleObject(done, 100);
+ ok(ret == WAIT_TIMEOUT, "got %#lx\n", ret);
+
+ CloseHandle(mutex);
+ ret = WaitForSingleObject(done, 100);
+ ok(ret == WAIT_TIMEOUT, "got %#lx\n", ret);
+
+ SetEvent(params[0].event);
+ ret = WaitForSingleObject(done, 1000);
+ ok(!ret, "got %#lx\n", ret);
+ ok(params[0].ret == WAIT_OBJECT_0, "got %#lx\n", params[0].ret);
+ /* let it block on the stop event before killing it */
+ ret = WaitForSingleObject(threads[0], 100);
+ ok(ret == WAIT_TIMEOUT, "got %#lx\n", ret);
+
+ TerminateThread(threads[0], 0);
+ ret = WaitForSingleObject(threads[0], 1000);
+ ok(!ret, "got %#lx\n", ret);
+
+ SetEvent(params[1].event);
+ ret = WaitForSingleObject(done, 6000);
+ ok(!ret, "got %#lx\n", ret);
+ ok(params[1].ret == WAIT_ABANDONED_0 || params[1].ret == WAIT_ABANDONED_0 + 1, "got %#lx\n", params[1].ret);
+
+ SetEvent(stop);
+ ret = WaitForSingleObject(threads[1], 1000);
+ ok(!ret, "got %#lx\n", ret);
+
+ for (i = 0; i < ARRAY_SIZE(params); i++)
+ {
+ CloseHandle(threads[i]);
+ CloseHandle(params[i].event);
+ }
+ CloseHandle(ready);
+ CloseHandle(done);
+ CloseHandle(stop);
+}
+
static void test_slist(void)
{
struct item
@@ -3550,6 +3636,7 @@ START_TEST(sync)
test_signalandwait();
test_temporary_objects();
test_mutex();
+ test_mutex_wait_all();
test_slist();
test_event();
test_semaphore();
@@ -0,0 +1,132 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Sun, 1 Nov 2026 14:57:35 -0800
Subject: [PATCH] kernel32/tests: Test that the stack of a killed thread is
freed.
A thread killed with TerminateThread(), while it waits or before it
ever ran, must not leave its stack allocated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/kernel32/tests/thread.c | 100 +++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)
diff --git a/dlls/kernel32/tests/thread.c b/dlls/kernel32/tests/thread.c
index 4fb49c93673..51683aba18f 100644
--- a/dlls/kernel32/tests/thread.c
+++ b/dlls/kernel32/tests/thread.c
@@ -648,6 +648,105 @@ static VOID test_TerminateThread(void)
ok(CloseHandle(thread)!=0,"Error Closing thread handle\n");
}
+static DWORD WINAPI thread_wait_forever( void *event )
+{
+ SetEvent( event );
+ for (;;) Sleep( INFINITE );
+ return 0;
+}
+
+static DWORD WINAPI thread_return( void *arg )
+{
+ return 0;
+}
+
+static void *get_thread_stack( HANDLE thread )
+{
+ THREAD_BASIC_INFORMATION info;
+ NTSTATUS status;
+ unsigned int i;
+
+ /* Wine knows the TEB of a new thread only once the thread has started up */
+ for (i = 0; i < 1000; i++)
+ {
+ status = pNtQueryInformationThread( thread, ThreadBasicInformation, &info, sizeof(info), NULL );
+ ok( !status, "NtQueryInformationThread failed, status %#lx.\n", status );
+ if (status) return NULL;
+ if (info.TebBaseAddress) return ((TEB *)info.TebBaseAddress)->DeallocationStack;
+ Sleep( 1 );
+ }
+ ok( 0, "got no TEB.\n" );
+ return NULL;
+}
+
+/* Windows frees the stack of a killed thread by the time the thread is signaled, Wine when
+ * another thread starts or exits: run threads until the stack is gone, or in use again. */
+static BOOL is_stack_freed( void *stack )
+{
+ MEMORY_BASIC_INFORMATION mbi;
+ HANDLE thread;
+ void *other;
+ unsigned int i;
+
+ for (i = 0; i < 50; i++)
+ {
+ if (!VirtualQuery( stack, &mbi, sizeof(mbi) )) return FALSE;
+ if (mbi.State == MEM_FREE || mbi.AllocationBase != stack) return TRUE;
+ thread = CreateThread( NULL, 0, thread_return, NULL, CREATE_SUSPENDED, NULL );
+ other = get_thread_stack( thread );
+ ResumeThread( thread );
+ WaitForSingleObject( thread, INFINITE );
+ CloseHandle( thread );
+ if (other == stack) return TRUE;
+ Sleep( 10 );
+ }
+ return FALSE;
+}
+
+static void test_TerminateThread_stack(void)
+{
+ HANDLE thread, event;
+ void *stack;
+ BOOL freed;
+ DWORD ret;
+
+ if (!pNtQueryInformationThread)
+ {
+ win_skip( "NtQueryInformationThread is not available.\n" );
+ return;
+ }
+
+ event = CreateEventW( NULL, FALSE, FALSE, NULL );
+ thread = CreateThread( NULL, 0, thread_wait_forever, event, 0, NULL );
+ ok( !!thread, "CreateThread failed, error %lu.\n", GetLastError() );
+ ret = WaitForSingleObject( event, 5000 );
+ ok( !ret, "thread did not start, ret %#lx.\n", ret );
+ stack = get_thread_stack( thread );
+ ok( !!stack, "got no stack.\n" );
+ ret = TerminateThread( thread, 1 );
+ ok( ret, "TerminateThread failed, error %lu.\n", GetLastError() );
+ ret = WaitForSingleObject( thread, 5000 );
+ ok( !ret, "thread did not end, ret %#lx.\n", ret );
+ CloseHandle( thread );
+ freed = stack && is_stack_freed( stack );
+ ok( freed, "stack %p of a thread killed while waiting was not freed.\n", stack );
+
+ /* killed before it ever ran */
+ thread = CreateThread( NULL, 0, thread_return, NULL, CREATE_SUSPENDED, NULL );
+ ok( !!thread, "CreateThread failed, error %lu.\n", GetLastError() );
+ stack = get_thread_stack( thread );
+ ok( !!stack, "got no stack.\n" );
+ ret = TerminateThread( thread, 1 );
+ ok( ret, "TerminateThread failed, error %lu.\n", GetLastError() );
+ ret = WaitForSingleObject( thread, 5000 );
+ ok( !ret, "thread did not end, ret %#lx.\n", ret );
+ CloseHandle( thread );
+ freed = stack && is_stack_freed( stack );
+ ok( freed, "stack %p of a thread killed before it ran was not freed.\n", stack );
+
+ CloseHandle( event );
+}
+
/* Check if CreateThread obeys the specified stack size. This code does
not work properly, and is currently disabled
*/
@@ -2915,6 +3014,7 @@ START_TEST(thread)
test_CreateThread_suspended();
test_SuspendThread();
test_TerminateThread();
+ test_TerminateThread_stack();
test_CreateThread_stack();
test_thread_priority();
test_GetCurrentThreadStackLimits();
@@ -0,0 +1,62 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Mon, 2 Nov 2026 19:36:51 -0800
Subject: [PATCH] ntdll: Don't keep the reply pipe's write end open during
thread init.
init_thread_pipe() sends the write end of a new thread's reply pipe to
the server. Since 44699c324f2 the thread closes its own copy only after
its init_thread or init_first_thread request has returned.
If the thread is killed after it has written that request but before
the server has read it, the server closes the thread's request pipe and
the reply fd it was sent, and never replies. The thread keeps waiting
for the reply, and since it holds the write end itself, the read never
sees end-of-file: the thread stays blocked for the life of the process,
with its pipes and its stacks. A thread killed just before it writes
the request gets EPIPE and exits, but leaks the write end.
Close the write end as soon as the server has it, as before 44699c324f2.
The fd number is still what the request passes: the server looks it up
among the fds this thread has sent.
Helldivers 2's anti-tamper code creates suspended threads and kills them
right away, several times a second, so these threads pile up during a
session.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/ntdll/unix/server.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/dlls/ntdll/unix/server.c b/dlls/ntdll/unix/server.c
index 8a02609bee1..0336bebc57f 100644
--- a/dlls/ntdll/unix/server.c
+++ b/dlls/ntdll/unix/server.c
@@ -1623,6 +1623,10 @@ static int init_thread_pipe(void)
wine_server_send_fd( reply_pipe[1] );
wine_server_send_fd( ntdll_get_thread_data()->wait_fd[1] );
ntdll_get_thread_data()->reply_fd = reply_pipe[0];
+ /* The server has its own copy now. If we kept ours until the init request
+ * returns, a thread killed before the server reads the request would wait
+ * for the reply forever. */
+ close( reply_pipe[1] );
return reply_pipe[1];
}
@@ -1780,7 +1784,6 @@ size_t server_init_process(void)
}
}
SERVER_END_REQ;
- close( reply_pipe );
if (ret) server_protocol_error( "init_first_thread failed with status %x\n", ret );
@@ -1899,7 +1902,6 @@ void server_init_thread( void *entry_point, BOOL *suspend )
*suspend = reply->suspend;
}
SERVER_END_REQ;
- close( reply_pipe );
}
NTSTATUS WINAPI NtAllocateReserveObject( HANDLE *handle, const OBJECT_ATTRIBUTES *attr,
@@ -0,0 +1,128 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Sun, 1 Nov 2026 14:57:35 -0800
Subject: [PATCH] ntdll: Free the TEB and stacks of a thread killed by another
thread.
A thread killed with TerminateThread() leaves through abort_thread(),
which never frees its TEB, its stack or its kernel stack: only
exit_thread() hands a thread's TEB on to the next exiting thread to
free. So every killed thread kept about 2 MiB mapped for good, and its
TEB stayed in the TEB list. Windows frees the initial stack of a
terminated thread.
A killed thread cannot free its own stacks, and abort_thread() may run
with a lock held, when the server connection breaks in the middle of a
request. So abort_thread() only queues the TEB, and the next thread
that is created or exits joins the killed thread and frees its TEB and
stacks.
Helldivers 2's anti-tamper code creates two suspended threads and kills
them every 0.4 seconds, which kept about 34 GiB of stacks mapped and
about 360 MiB of memory resident per hour.
GE: use pthread_tryjoin_np() and queue threads that haven't finished
exiting for a later pass, so a killed thread stuck in its unix-side exit
(e.g. on a library lock) can't hang the next CreateThread() or thread exit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
--- a/dlls/ntdll/unix/thread.c
+++ b/dlls/ntdll/unix/thread.c
@@ -85,6 +85,7 @@
pthread_key_t teb_key = 0;
static LONG nb_threads = 1;
+static TEB *killed_threads; /* killed threads whose TEB and stacks are still to be freed */
static inline int get_unix_exit_code( NTSTATUS status )
{
@@ -1147,6 +1148,40 @@
/***********************************************************************
+ * free_killed_threads
+ *
+ * Free the TEB and stacks of the threads that abort_thread() queued.
+ */
+static void free_killed_threads(void)
+{
+ TEB *teb = InterlockedExchangePointer( (void **)&killed_threads, NULL );
+
+ while (teb)
+ {
+ struct ntdll_thread_data *thread_data = (struct ntdll_thread_data *)&teb->GdiTebBatch;
+ TEB *next = thread_data->next_killed;
+
+#ifdef __GLIBC__
+ /* A thread killed in a unix call may hold a library lock and not have
+ * finished exiting yet. Don't wait for it: queue it for a later pass. */
+ if (pthread_tryjoin_np( thread_data->pthread_id, NULL ) == EBUSY)
+ {
+ do thread_data->next_killed = killed_threads;
+ while (InterlockedCompareExchangePointer( (void **)&killed_threads, teb,
+ thread_data->next_killed ) != thread_data->next_killed);
+ teb = next;
+ continue;
+ }
+#else
+ pthread_join( thread_data->pthread_id, NULL );
+#endif
+ virtual_free_teb( teb );
+ teb = next;
+ }
+}
+
+
+/***********************************************************************
* get_machine_context_size
*/
static SIZE_T get_machine_context_size( USHORT machine )
@@ -1432,6 +1467,8 @@
pthread_sigmask( SIG_BLOCK, &server_block_set, &sigset );
+ free_killed_threads();
+
if ((status = virtual_alloc_teb( &teb ))) goto done;
if ((status = init_thread_stack( teb, get_zero_bits_limit( zero_bits ), stack_reserve, stack_commit )))
@@ -1611,8 +1648,19 @@
*/
void abort_thread( int status )
{
+ struct ntdll_thread_data *thread_data = ntdll_get_thread_data();
+
pthread_sigmask( SIG_BLOCK, &server_block_set, NULL );
if (InterlockedDecrement( &nb_threads ) <= 0) abort_process( status );
+
+ /* A killed thread cannot free its own stacks, and it may hold a lock here: leave them
+ * to the next thread that is created or exits. */
+ if (thread_data->pthread_id)
+ {
+ do thread_data->next_killed = killed_threads;
+ while (InterlockedCompareExchangePointer( (void **)&killed_threads, NtCurrentTeb(),
+ thread_data->next_killed ) != thread_data->next_killed);
+ }
pthread_exit_wrapper( status );
}
@@ -1642,6 +1690,8 @@
if (InterlockedDecrement( &nb_threads ) <= 0) exit_process( status );
+ free_killed_threads();
+
if ((teb = InterlockedExchangePointer( &prev_teb, NtCurrentTeb() )))
{
struct ntdll_thread_data *thread_data = (struct ntdll_thread_data *)&teb->GdiTebBatch;
--- a/dlls/ntdll/unix/unix_private.h
+++ b/dlls/ntdll/unix/unix_private.h
@@ -118,6 +118,7 @@
void *jmp_buf; /* setjmp buffer for exception handling */
BOOL system_thread; /* thread runs only on the Unix side */
int *fsync_apc_futex;
+ TEB *next_killed; /* next in the list of killed threads to free */
};
C_ASSERT( sizeof(struct ntdll_thread_data) <= sizeof(((TEB *)0)->GdiTebBatch) );
@@ -0,0 +1,110 @@
From 9fb89c9045f152b753b8a5a0ad5518ec15f4c528 Mon Sep 17 00:00:00 2001
From: Grigory Romodanovskiy <[email protected]>
Date: Wed, 28 Oct 2026 18:21:40 -0400
Subject: [PATCH] ntdll: Reuse free blocks of partially used LFH groups.
A group was taken off its bin when all its blocks were allocated, and
was only put back when all of them were freed again. A single long lived
block kept all the other blocks of the group unusable, which made the
heap grow without bound with some allocation patterns.
Clear GROUP_FLAG_FREE together with setting the first block free bit
instead, and put the group back in its bin from the thread that cleared
it. As the thread freeing the last block no longer owns the group,
release surplus fully freed groups when they are popped from the bin.
Proton: keep Proton's bin->group_max as the limit for releasing fully
freed groups, as in "ntdll: Keep more LFH block groups alive", so only the
partially used group reuse changes here. (GE: kept on merge; the PR used
the upstream ARRAY_SIZE(affinity_mapping) limit.)
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=60589
Upstream: https://gitlab.winehq.org/wine/wine/-/merge_requests/12472
---
diff --git a/dlls/ntdll/heap.c b/dlls/ntdll/heap.c
index 1e99799da13..c7f81323771 100644
--- a/dlls/ntdll/heap.c
+++ b/dlls/ntdll/heap.c
@@ -1880,34 +1880,35 @@ static struct group *heap_acquire_bin_group( struct heap *heap, ULONG flags, SIZ
if ((group = InterlockedExchangePointer( (void *)bin_get_affinity_group( bin, affinity ), NULL )))
return group;
- if ((entry = RtlInterlockedPopEntrySList( &bin->groups )))
- return CONTAINING_RECORD( entry, struct group, entry );
+ while ((entry = RtlInterlockedPopEntrySList( &bin->groups )))
+ {
+ group = CONTAINING_RECORD( entry, struct group, entry );
+
+ /* release fully freed groups if the bin has enough of them */
+ if (ReadAcquire( &group->free_bits ) != ~GROUP_FLAG_FREE) return group;
+ if (RtlQueryDepthSList( &bin->groups ) <= ReadAcquire( &bin->group_max )) return group;
+
+ InterlockedIncrement( &bin->group_freed );
+ group_release( heap, flags, bin, group );
+ }
count = InterlockedIncrement( &bin->group_alloc ) - ReadNoFence( &bin->group_freed );
if (count > ReadAcquire( &bin->group_max )) WriteRelease( &bin->group_max, count );
return group_allocate( heap, flags, block_size );
}
-/* release a thread owned and fully freed group to the bin shared group, or free its memory */
-static NTSTATUS heap_release_bin_group( struct heap *heap, ULONG flags, struct bin *bin, struct group *group )
+/* release a thread owned group with free blocks to its affinity group, or to the bin shared groups */
+static void heap_release_bin_group( struct bin *bin, struct group *group )
{
ULONG affinity = group->affinity;
/* using InterlockedExchangePointer here would possibly return a group that has used blocks,
- * we prefer keeping our fully freed group instead for reduced memory consumption.
+ * we prefer keeping our group instead for reduced memory consumption.
*/
if (!InterlockedCompareExchangePointer( (void *)bin_get_affinity_group( bin, affinity ), group, NULL ))
- return STATUS_SUCCESS;
+ return;
- /* try re-using the block group instead of releasing it */
- if (RtlQueryDepthSList( &bin->groups ) <= ReadAcquire( &bin->group_max ))
- {
- RtlInterlockedPushEntrySList( &bin->groups, &group->entry );
- return STATUS_SUCCESS;
- }
-
- InterlockedIncrement( &bin->group_freed );
- return group_release( heap, flags, bin, group );
+ RtlInterlockedPushEntrySList( &bin->groups, &group->entry );
}
static struct block *find_free_bin_block( struct heap *heap, ULONG flags, SIZE_T block_size, struct bin *bin )
@@ -1967,7 +1968,7 @@ static NTSTATUS heap_free_block_lfh( struct heap *heap, ULONG flags, struct bloc
struct bin *bin, *last = heap->bins + BLOCK_SIZE_BIN_COUNT - 1;
SIZE_T i, block_size = block_get_size( block );
struct group *group = block_get_group( block );
- NTSTATUS status = STATUS_SUCCESS;
+ LONG old, prev;
if (!(block_get_flags( block ) & BLOCK_FLAG_LFH)) return STATUS_UNSUCCESSFUL;
@@ -1980,15 +1981,14 @@ static NTSTATUS heap_free_block_lfh( struct heap *heap, ULONG flags, struct bloc
block_set_flags( block, (BYTE)~BLOCK_FLAG_LFH, BLOCK_FLAG_FREE );
mark_block_free( block + 1, (char *)block + block_size - (char *)(block + 1), flags );
- /* if this was the last used block in a group and GROUP_FLAG_FREE was set */
- if (InterlockedOr( &group->free_bits, 1 << i ) == ~(1 << i))
- {
- /* thread now owns the group, and can release it to its bin */
- group->free_bits = ~GROUP_FLAG_FREE;
- status = heap_release_bin_group( heap, flags, bin, group );
- }
+ /* set the block free bit, and clear GROUP_FLAG_FREE if the group was unlinked */
+ old = ReadNoFence( &group->free_bits );
+ while ((prev = InterlockedCompareExchange( &group->free_bits, (old | (1 << i)) & ~GROUP_FLAG_FREE, old )) != old)
+ old = prev;
- return status;
+ /* thread now owns the group, and can put it back in its bin */
+ if (old & GROUP_FLAG_FREE) heap_release_bin_group( bin, group );
+ return STATUS_SUCCESS;
}
static void bin_try_enable( struct heap *heap, struct bin *bin )
@@ -0,0 +1,75 @@
From: GloriousEggroll <[email protected]>
Date: Thu, 22 Oct 2026 15:00:00 -0600
Subject: [PATCH] ntdll: Keep the address range the NVIDIA driver reserves for DLSS free.
When a Vulkan device is created with VK_NVX_binary_import /
VK_NVX_image_view_handle, which DLSS needs, the NVIDIA driver (libcuda)
looks for free space in /proc/self/maps from 0x200000000 up to 1T (16T in
one mode on newer drivers) and maps 4G + 2M at the first fit. With
ntdll-ForceBottomUpAlloc, large allocations land right at 0x200000000, so a
game that reserves enough memory early (Star Citizen allocates 1T) leaves
no room, vkCreateDevice fails, DXVK and vkd3d-proton fall back to a device
without those extensions and DLSS is unavailable.
The mapping was 0x200000000-0x300200000 on all seven GPUs measured, from
GTX 1660 SUPER to RTX 5080 (4-16G VRAM, 15-62G RAM, 580-615 drivers,
including hybrid and multi-GPU systems). Keep exactly that range out of
the free areas on 64-bit so the driver always finds it, while bottom-up
order is kept for everything else. Allocations at an explicit address are
still allowed there.
---
diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c
--- a/dlls/ntdll/unix/virtual.c
+++ b/dlls/ntdll/unix/virtual.c
@@ -2285,6 +2285,34 @@
return try_map_free_area_range( area, base, end );
}
+#ifdef _WIN64
+/* The NVIDIA driver reserves 4G + 2M at the first free address from 0x200000000 when a
+ * Vulkan device uses the extensions DLSS needs, and device creation fails if 0x200000000
+ * up to its search limit is taken. Bottom-up allocation would fill exactly that range, so
+ * keep it out of the free areas (but unmapped, the driver looks for it in /proc/self/maps). */
+static char * const blocked_area_start = (char *)0x200000000;
+static char * const blocked_area_end = (char *)0x300200000;
+#else
+static char * const blocked_area_start = NULL;
+static char * const blocked_area_end = NULL;
+#endif
+
+static void *alloc_unblocked_area_in_range( struct alloc_area *area, char *base, char *end )
+{
+ char *result;
+
+ if (blocked_area_end <= base || blocked_area_start >= end)
+ return alloc_free_area_in_range( area, base, end );
+
+ if (area->top_down)
+ {
+ if ((result = alloc_free_area_in_range( area, max( base, blocked_area_end ), end ))) return result;
+ return alloc_free_area_in_range( area, base, min( end, blocked_area_start ));
+ }
+ if ((result = alloc_free_area_in_range( area, base, min( end, blocked_area_start )))) return result;
+ return alloc_free_area_in_range( area, max( base, blocked_area_end ), end );
+}
+
static void *alloc_free_area( char *limit_low, char *limit_high, size_t size, BOOL top_down, int unix_prot, UINT_PTR align_mask )
{
struct range_entry *range, *ranges_start, *ranges_end;
@@ -2348,14 +2376,14 @@
else
{
/* range is split in two by the preloader reservation, try first part. */
- if ((result = alloc_free_area_in_range( &area, base, reserve_start )))
+ if ((result = alloc_unblocked_area_in_range( &area, base, reserve_start )))
break;
/* then fall through to try second part. */
base = reserve_end;
}
}
- if ((result = alloc_free_area_in_range( &area, base, end )))
+ if ((result = alloc_unblocked_area_in_range( &area, base, end )))
break;
}
@@ -0,0 +1,39 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Tue, 20 Oct 2026 18:30:00 -0600
Subject: [PATCH] ntdll: HACK: Run NCSOFT PURPLE's purple-agent.exe in new WoW64
mode.
PURPLE 2.26.1019.7 ships a VMProtect-packed purple-agent.exe. In old-style
WoW64 its loader queries ProcessWow64Information and then fails with
"Initialization Error 1", once for every agent the launcher spawns. The
same binary starts normally in new WoW64 mode (WINEARCH=wow64).
PURPLE launches many games, so key on the agent rather than an app ID:
when spawning a 32-bit purple-agent.exe on a 64-bit host, set
WINEARCH=wow64 for that process only. The launcher and the games keep
their default mode.
https://github.com/GloriousEggroll/proton-ge-custom/issues/870
---
diff --git a/dlls/ntdll/unix/loader.c b/dlls/ntdll/unix/loader.c
--- a/dlls/ntdll/unix/loader.c
+++ b/dlls/ntdll/unix/loader.c
@@ -581,6 +581,17 @@
if (ld_preload) setenv( "WINE_LD_PRELOAD", ld_preload, 1 );
+ /* HACK: NCSOFT PURPLE's VMProtect-packed purple-agent.exe fails its loader check
+ * ("Initialization Error 1") in old-style WoW64, but runs in new WoW64 mode.
+ * The agent is spawned by the launcher for every PURPLE game, so key on it. */
+ if (machine == IMAGE_FILE_MACHINE_I386 && is_machine_64bit( native_machine ) && argv[2])
+ {
+ const char *name = argv[2], *p;
+
+ for (p = argv[2]; *p; p++) if (*p == '\\' || *p == '/') name = p + 1;
+ if (!strcasecmp( name, "purple-agent.exe" )) setenv( "WINEARCH", "wow64", 1 );
+ }
+
signal( SIGPIPE, SIG_DFL );
snprintf( socket_env, sizeof(socket_env), "WINESERVERSOCKET=%u", socketfd );
@@ -1,39 +1,47 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Tue, 29 Sep 2026 19:36:30 -0700
Subject: [PATCH] ntdll: Take the in-flight alert when a RtlWaitOnAddress()
wait times out after being dequeued.
Subject: [PATCH] ntdll: Take the alert of a RtlWaitOnAddress() wake that races
a timeout.
RtlWakeAddressSingle() and RtlWakeAddressAll() dequeue a waiter under the
queue lock and alert it after releasing the lock. A waiter whose wait
times out in between finds itself dequeued but returns STATUS_TIMEOUT.
Its alert then arrives with nobody waiting for it and stays pending, and
the thread's next NtWaitForAlertByThreadId() returns at once, whatever it
waits for: typically a SleepConditionVariableCS() or
SleepConditionVariableSRW() with an infinite timeout on an unrelated
condition variable, which wakes up spuriously.
RtlWakeAddressSingle() and RtlWakeAddressAll() dequeue a waiter under
the queue lock and alert it after releasing the lock. A waiter whose
wait times out in between finds itself dequeued but returns
STATUS_TIMEOUT. Its alert then arrives with nobody waiting for it and
stays pending on the thread, and the thread's next
NtWaitForAlertByThreadId() returns at once whatever it waits for:
typically a SleepConditionVariableCS() or SleepConditionVariableSRW()
with an infinite timeout on an unrelated condition variable, which
wakes up spuriously. The comment in RtlWakeAddressAll() accepts that
as the price of not alerting under the spinlock.
Windows doesn't leave that alert behind: a waiter that finds itself
claimed by a waker after its timeout waits again for the wake before
returning. Do the same. A dequeued waiter has been woken, so take the
alert and report the wake.
Windows leaves no alert pending after such a race: a waiter that a
waker has claimed takes the wake before returning. Do the same: a
waiter that finds itself dequeued when its wait timed out has been
woken, so take the in-flight alert with an untimed
NtWaitForAlertByThreadId() and report the wake. The wakers still alert
outside the lock, and the second wait lasts only until the waker's
alert, which follows its unlock. Update the comment.
Warhammer 40,000: Space Marine 2 crashes under Wine because of this. Its tcmalloc spinlock back-off polls with zero-timeout
WaitOnAddress() calls, whose timeouts race the releasing
WakeByAddressSingle(), and the game's unlooped
SleepConditionVariableCS(INFINITE) then returns early. On Windows 11 the
same stress program shows no early return in 100,000 waits, against 13%
under Wine.
Zero-timeout waits make the race common: they are queued like any
other wait and time out immediately, and spinlock back-offs that poll
with WaitOnAddress(..., 0), such as tcmalloc's, collect the stale
alert. Warhammer 40,000: Space Marine 2 crashes because of this when
an unlooped SleepConditionVariableCS(INFINITE) returns while file
reads are still pending.
Backported from https://github.com/jegglest/wine/commit/dee180fa83 and refreshed for
the ReadPointerAcquire() cast in Valve's b2a60dcd145.
https://github.com/ValveSoftware/Proton/issues/8072
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=60523
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/ntdll/sync.c | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/dlls/ntdll/sync.c b/dlls/ntdll/sync.c
index 729d7cbd847..cc05308c715 100644
--- a/dlls/ntdll/sync.c
+++ b/dlls/ntdll/sync.c
@@ -883,6 +883,7 @@
@@ -883,6 +883,7 @@ NTSTATUS WINAPI RtlWaitOnAddress( const void *addr, const void *cmp, SIZE_T size
{
struct futex_queue *queue = get_futex_queue( addr );
struct futex_entry entry;
@@ -41,25 +49,31 @@ diff --git a/dlls/ntdll/sync.c b/dlls/ntdll/sync.c
NTSTATUS ret;
TRACE("addr %p cmp %p size %#Ix timeout %s\n", addr, cmp, size, debugstr_timeout( timeout ));
@@ -915,11 +916,20 @@
@@ -915,11 +916,15 @@ NTSTATUS WINAPI RtlWaitOnAddress( const void *addr, const void *cmp, SIZE_T size
if (ReadPointerAcquire( (void * volatile *)&entry.addr ))
{
spin_lock( &queue->lock );
- if (entry.addr)
+ if ((queued = entry.addr != NULL))
+ if ((queued = !!entry.addr))
list_remove( &entry.entry );
spin_unlock( &queue->lock );
}
+ if (!queued && ret == STATUS_TIMEOUT)
+ {
+ /* A waker removed us as the wait timed out: we have been woken, and the waker alerts us right after
+ * releasing the queue lock. Take that alert, as Windows does, or it would end this thread's next wait
+ * early, whatever it waits for. */
+ NtWaitForAlertByThreadId( NULL, NULL );
+ ret = STATUS_SUCCESS;
+ }
+ /* A waker took us off the queue and alerts us after releasing the lock. If our wait timed out first, take
+ * that alert, or it would end this thread's next wait early. */
+ if (!queued && ret == STATUS_TIMEOUT) ret = NtWaitForAlertByThreadId( NULL, NULL );
+
TRACE("returning %#lx\n", ret);
if (ret == STATUS_ALERTED) ret = STATUS_SUCCESS;
@@ -962,8 +967,8 @@ void WINAPI RtlWakeAddressAll( const void *addr )
}
}
- /* Try not to make a system call while holding a spinlock (even if that can be responsible for spurious wake
- * up scenario). */
+ /* Try not to make a system call while holding a spinlock. A waiter whose wait times out before the alert
+ * arrives takes it in RtlWaitOnAddress(). */
spin_unlock( &queue->lock );
if (count)
NtAlertMultipleThreadByThreadId( tids, count, NULL, NULL );
@@ -0,0 +1,104 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Tue, 29 Sep 2026 19:36:30 -0700
Subject: [PATCH] ntdll/tests: Test a RtlWaitOnAddress() wake racing a
timed-out wait.
A wake that races the end of a short wait may be taken by that wait,
but it must not leave an alert pending on the thread, which would end
the thread's next wait on anything early.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/ntdll/tests/sync.c | 70 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 70 insertions(+)
diff --git a/dlls/ntdll/tests/sync.c b/dlls/ntdll/tests/sync.c
index dcbb8b18c3f..5b8f5dd987a 100644
--- a/dlls/ntdll/tests/sync.c
+++ b/dlls/ntdll/tests/sync.c
@@ -615,6 +615,75 @@ static void test_wait_on_address(void)
ok(address == 0, "got %s\n", wine_dbgstr_longlong(address));
}
+struct wait_on_address_waker
+{
+ LONG address;
+ volatile LONG stop;
+ BOOL wake_all;
+};
+
+static DWORD WINAPI wait_on_address_waker_thread( void *arg )
+{
+ struct wait_on_address_waker *waker = arg;
+
+ while (!waker->stop)
+ {
+ if (waker->wake_all) pRtlWakeAddressAll( &waker->address );
+ else pRtlWakeAddressSingle( &waker->address );
+ }
+ return 0;
+}
+
+static void test_wait_on_address_raced_wake(void)
+{
+ static const LONGLONG timeouts[] = {0, -1};
+ static const LARGE_INTEGER zero;
+ struct wait_on_address_waker waker;
+ unsigned int i, count, pending, failed;
+ LARGE_INTEGER timeout;
+ LONG compare = 0;
+ NTSTATUS status;
+ HANDLE thread;
+ DWORD start;
+
+ if (!pRtlWaitOnAddress || !pNtWaitForAlertByThreadId)
+ {
+ win_skip("RtlWaitOnAddress or NtWaitForAlertByThreadId not supported, skipping test\n");
+ return;
+ }
+
+ /* A wake that races the end of a short wait may be taken by that wait, but it must not leave an alert
+ * pending on the thread, which would end its next wait on anything early. */
+ for (i = 0; i < ARRAY_SIZE(timeouts) * 2; i++)
+ {
+ winetest_push_context( "timeout %s, %s", wine_dbgstr_longlong( timeouts[i / 2] ),
+ i % 2 ? "wake all" : "wake single" );
+
+ waker.address = 0;
+ waker.stop = 0;
+ waker.wake_all = i % 2;
+ thread = CreateThread( NULL, 0, wait_on_address_waker_thread, &waker, 0, NULL );
+
+ pending = failed = 0;
+ timeout.QuadPart = timeouts[i / 2];
+ start = GetTickCount();
+ for (count = 0; count < 20000 && GetTickCount() - start < 1000; count++)
+ {
+ status = pRtlWaitOnAddress( &waker.address, &compare, sizeof(compare), &timeout );
+ if (status != STATUS_SUCCESS && status != STATUS_TIMEOUT) failed++;
+ if (pNtWaitForAlertByThreadId( NULL, &zero ) == STATUS_ALERTED) pending++;
+ }
+
+ waker.stop = 1;
+ WaitForSingleObject( thread, INFINITE );
+ CloseHandle( thread );
+
+ ok( !failed, "%u of %u waits failed\n", failed, count );
+ ok( !pending, "%u of %u waits left an alert pending\n", pending, count );
+ winetest_pop_context();
+ }
+}
+
static HANDLE thread_ready, thread_done;
static DWORD WINAPI resource_shared_thread(void *arg)
@@ -1474,6 +1543,7 @@ START_TEST(sync)
pRtlBarrier = (void *)GetProcAddress(module, "RtlBarrier");
test_wait_on_address();
+ test_wait_on_address_raced_wake();
test_event();
test_mutant();
test_semaphore();
@@ -0,0 +1,73 @@
From: GloriousEggroll <[email protected]>
Date: Mon, 2 Nov 2026 20:00:00 -0700
Subject: [PATCH] rtworkq: Don't hold queues_section while shutting down the
system queues.
shutdown_system_queues() held queues_section across shutdown_queue(),
which waits for the running pool callbacks. A callback that unlocks a
user queue takes queues_section too, so MFShutdown() deadlocked with it:
thread A: MFShutdown -> RtwqShutdown -> shutdown_system_queues
(holds queues_section, waits for the callback)
thread B: source reader callback -> RtwqUnlockWorkQueue
(waits for queues_section)
unlock_user_queue() already shuts its queue down outside the lock.
Shut the standard queue down first, without the lock. grab_queue() then
fails, and it now rechecks that under the lock, so no new system queue
is created behind us; taking the lock once waits for one still being
created. Then shut the other system queues down, also without the lock.
https://github.com/GloriousEggroll/proton-ge-custom/issues/899
---
--- a/dlls/rtworkq/queue.c
+++ b/dlls/rtworkq/queue.c
@@ -665,6 +665,18 @@
struct queue_desc desc;
EnterCriticalSection(&queues_section);
+ /* Don't create a system queue after shutdown_system_queues() went past it. */
+ if (!system_queues[SYS_QUEUE_STANDARD].pool)
+ {
+ LeaveCriticalSection(&queues_section);
+ return RTWQ_E_SHUTDOWN;
+ }
+ if (queue->pool)
+ {
+ LeaveCriticalSection(&queues_section);
+ *ret = queue;
+ return S_OK;
+ }
switch (queue_id)
{
case RTWQ_CALLBACK_QUEUE_IO:
@@ -1310,17 +1322,26 @@
unsigned int i;
HRESULT hr;
+ /* Shutting down a pool queue waits for its running callbacks, which may
+ * need queues_section themselves, e.g. to unlock a user queue. Don't hold
+ * it while waiting.
+ *
+ * Once the standard queue is shut down, grab_queue() fails and creates no
+ * more system queues. Taking the lock waits for one that is still being
+ * created, so that it is shut down below. */
+ shutdown_queue(&system_queues[SYS_QUEUE_STANDARD]);
+
EnterCriticalSection(&queues_section);
+ LeaveCriticalSection(&queues_section);
for (i = 0; i < ARRAY_SIZE(system_queues); ++i)
{
+ if (i == SYS_QUEUE_STANDARD) continue;
shutdown_queue(&system_queues[i]);
}
if (FAILED(hr = CoDecrementMTAUsage(mta_cookie)))
WARN("Failed to uninitialize MTA, hr %#lx.\n", hr);
-
- LeaveCriticalSection(&queues_section);
}
HRESULT WINAPI RtwqShutdown(void)
@@ -0,0 +1,334 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Wed, 30 Sep 2026 16:12:23 -0700
Subject: [PATCH] server: Keep an in-process mutex a client closed while still
using it.
A thread can be waiting on a mutex whose last handle its process has
closed: the wait holds a duplicate of the ntsync fd, and it can still
acquire the mutex later, for instance in a wait-all whose other objects
are signaled afterwards. The server only sends NTSYNC_IOC_MUTEX_KILL to
the mutexes it still has objects for, so if that thread then dies the
mutex is never abandoned and other waiters block forever. The previous
commit covers mutexes that are owned when their object is destroyed,
not those acquired afterwards.
The cache entry of an in-process sync counts its users, so
close_inproc_sync() can tell when another thread is still using a
mutex. It then asks the server, with the new keep_inproc_sync request
sent before the handle is closed, to keep a reference to the in-process
sync under the entry's serial; the entry's last user drops it with
release_kept_inproc_sync. A process's records go away with the process.
The comment above struct inproc_sync already describes the client
keeping the object until its last user is done; this does so only when
it matters: closing a handle that nobody else is using, the common
case, sends no extra request.
The serial makes struct inproc_sync 20 bytes, which no longer divides
the 64K cache blocks evenly, and anon_mmap_alloc() asserts that its
size is a multiple of the page size: map whole 64K blocks rather than
the entries' exact size.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=60524
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
dlls/ntdll/unix/sync.c | 54 ++++++++++++++++++++++---
server/inproc_sync.c | 91 ++++++++++++++++++++++++++++++++++++++++++
server/object.h | 1 +
server/process.c | 2 +
server/protocol.def | 16 ++++++++
5 files changed, 159 insertions(+), 5 deletions(-)
diff --git a/dlls/ntdll/unix/sync.c b/dlls/ntdll/unix/sync.c
index f8d0a95611a..d4edbfd7ada 100644
--- a/dlls/ntdll/unix/sync.c
+++ b/dlls/ntdll/unix/sync.c
@@ -542,9 +542,14 @@ struct inproc_sync
unsigned int access; /* handle access rights */
unsigned short type; /* enum inproc_sync_type as short to save space */
unsigned short closed; /* fd has been closed but sync is still referenced */
+ unsigned int serial; /* id of this cache entry for keep_inproc_sync, 0 if none */
};
-#define INPROC_SYNC_CACHE_BLOCK_SIZE (65536 / sizeof(struct inproc_sync))
+static LONG next_inproc_sync_serial;
+
+/* a cache block is 64K, a multiple of any host page size; the entries need not fill it */
+#define INPROC_SYNC_CACHE_BLOCK_BYTES 65536
+#define INPROC_SYNC_CACHE_BLOCK_SIZE (INPROC_SYNC_CACHE_BLOCK_BYTES / sizeof(struct inproc_sync))
#define INPROC_SYNC_CACHE_ENTRIES 128
static struct inproc_sync *inproc_sync_cache[INPROC_SYNC_CACHE_ENTRIES];
@@ -582,8 +587,8 @@ static struct inproc_sync *cache_inproc_sync( HANDLE handle, struct inproc_sync
if (!entry) inproc_sync_cache[0] = inproc_sync_cache_initial_block;
else
{
- static const size_t size = INPROC_SYNC_CACHE_BLOCK_SIZE * sizeof(struct inproc_sync);
- void *ptr = anon_mmap_alloc( size, PROT_READ | PROT_WRITE );
+ /* map the whole block: mmap() needs whole pages, and the entries don't fill it exactly */
+ void *ptr = anon_mmap_alloc( INPROC_SYNC_CACHE_BLOCK_BYTES, PROT_READ | PROT_WRITE );
if (ptr == MAP_FAILED) return sync;
inproc_sync_cache[entry] = ptr;
}
@@ -604,6 +609,7 @@ static struct inproc_sync *cache_inproc_sync( HANDLE handle, struct inproc_sync
cache->access = sync->access;
cache->type = sync->type;
cache->closed = sync->closed;
+ while (!(cache->serial = InterlockedIncrement( &next_inproc_sync_serial ))) ;
/* Make sure we set the other members before the refcount; this store needs
* release semantics [paired with the load in get_cached_inproc_sync()].
* Set the refcount to 2 (one for the handle, one for the caller). */
@@ -628,15 +634,30 @@ static inline LONG interlocked_inc_if_nonzero( LONG *dest )
return val;
}
+static void release_kept_inproc_sync( unsigned int serial )
+{
+ SERVER_START_REQ( release_kept_inproc_sync )
+ {
+ req->serial = serial;
+ wine_server_call( req );
+ }
+ SERVER_END_REQ;
+}
+
static void release_inproc_sync( struct inproc_sync *sync )
{
- /* save the fd now; as soon as the refcount hits 0 we cannot
+ /* save the fd and serial now; as soon as the refcount hits 0 we cannot
* access the cache anymore */
int fd = sync->fd;
+ unsigned int serial = sync->serial;
LONG ref = InterlockedDecrement( &sync->refcount );
assert( ref >= 0 );
- if (!ref) close( fd );
+ if (ref) return;
+ close( fd );
+ /* The handle was closed while the object was in use, and close_inproc_sync()
+ * had the server keep the object under this serial: let it go now. */
+ if (serial) release_kept_inproc_sync( serial );
}
static struct inproc_sync *get_cached_inproc_sync( HANDLE handle )
@@ -682,6 +703,7 @@ static NTSTATUS get_server_inproc_sync( HANDLE handle, struct inproc_sync *sync
sync->access = reply->access;
sync->type = reply->type;
sync->closed = 0;
+ sync->serial = 0;
}
}
SERVER_END_REQ;
@@ -760,6 +782,20 @@ extern NTSTATUS check_signal_access( struct inproc_sync *sync )
return STATUS_OBJECT_TYPE_MISMATCH;
}
+static NTSTATUS keep_inproc_sync( HANDLE handle, unsigned int serial )
+{
+ NTSTATUS ret;
+
+ SERVER_START_REQ( keep_inproc_sync )
+ {
+ req->handle = wine_server_obj_handle( handle );
+ req->serial = serial;
+ ret = wine_server_call( req );
+ }
+ SERVER_END_REQ;
+ return ret;
+}
+
/* caller must hold fd_cache_mutex */
void close_inproc_sync( HANDLE handle )
{
@@ -774,6 +810,14 @@ void close_inproc_sync( HANDLE handle )
if ((cache = get_cached_inproc_sync( handle )))
{
cache->closed = 1;
+ /* One reference is the handle's and one is ours; any other belongs to a
+ * thread in the middle of an operation, typically a wait, which can still
+ * acquire a mutex through the fd. The server must then still know the
+ * mutex to abandon it if that thread dies, so have it keep the mutex under
+ * this entry's serial until the last user releases the entry. */
+ if (cache->type != INPROC_SYNC_MUTEX || InterlockedCompareExchange( &cache->refcount, 0, 0 ) <= 2
+ || keep_inproc_sync( handle, cache->serial ))
+ cache->serial = 0;
/* once for the reference we just grabbed, and once for the handle */
release_inproc_sync( cache );
release_inproc_sync( cache );
diff --git a/server/inproc_sync.c b/server/inproc_sync.c
index 7199915ac76..f3fe5a37d59 100644
--- a/server/inproc_sync.c
+++ b/server/inproc_sync.c
@@ -107,6 +107,20 @@ static void prune_orphan_mutexes(void)
}
}
+/* Mutexes whose handle a process closed while one of its threads was still using the cached fd (typically a
+ * wait in progress). That thread can still acquire the mutex through the fd, so the server keeps the object,
+ * and with it the mutex's place on inproc_mutexes, until the client reports that the last user is done, or
+ * the process dies. */
+struct kept_sync
+{
+ struct list entry;
+ struct process *process; /* process that closed the handle; the record dies with it */
+ unsigned int serial; /* id chosen by the client, unique within the process */
+ struct inproc_sync *sync; /* the reference this record holds */
+};
+
+static struct list kept_syncs = LIST_INIT( kept_syncs );
+
static void inproc_sync_dump( struct object *obj, int verbose );
static int inproc_sync_signal( struct object *obj, unsigned int access, int signal );
static void inproc_sync_destroy( struct object *obj );
@@ -328,6 +342,55 @@ void abandon_inproc_mutexes( thread_id_t tid )
prune_orphan_mutexes();
}
+static void keep_inproc_mutex( struct process *process, struct object *obj, unsigned int serial )
+{
+ struct object *sync;
+ struct kept_sync *kept;
+
+ if (!(sync = get_obj_sync( obj ))) return;
+ if (sync->ops == &inproc_sync_ops && ((struct inproc_sync *)sync)->type == INPROC_SYNC_MUTEX)
+ {
+ if ((kept = mem_alloc( sizeof(*kept) )))
+ {
+ kept->process = process;
+ kept->serial = serial;
+ kept->sync = (struct inproc_sync *)sync; /* keeps the reference from get_obj_sync() */
+ list_add_tail( &kept_syncs, &kept->entry );
+ return;
+ }
+ }
+ else set_error( STATUS_OBJECT_TYPE_MISMATCH );
+ release_object( sync );
+}
+
+static void release_kept_inproc_mutex( struct process *process, unsigned int serial )
+{
+ struct kept_sync *kept;
+
+ LIST_FOR_EACH_ENTRY( kept, &kept_syncs, struct kept_sync, entry )
+ {
+ if (kept->process != process || kept->serial != serial) continue;
+ list_remove( &kept->entry );
+ release_object( kept->sync );
+ free( kept );
+ return;
+ }
+ set_error( STATUS_INVALID_PARAMETER );
+}
+
+void release_kept_inproc_syncs( struct process *process )
+{
+ struct kept_sync *kept, *next;
+
+ LIST_FOR_EACH_ENTRY_SAFE( kept, next, &kept_syncs, struct kept_sync, entry )
+ {
+ if (kept->process != process) continue;
+ list_remove( &kept->entry );
+ release_object( kept->sync );
+ free( kept );
+ }
+}
+
static int get_obj_inproc_sync( struct object *obj, int *type )
{
struct object *sync;
@@ -389,6 +452,20 @@ void abandon_inproc_mutexes( thread_id_t tid )
{
}
+static void keep_inproc_mutex( struct process *process, struct object *obj, unsigned int serial )
+{
+ set_error( STATUS_NOT_IMPLEMENTED );
+}
+
+static void release_kept_inproc_mutex( struct process *process, unsigned int serial )
+{
+ set_error( STATUS_NOT_IMPLEMENTED );
+}
+
+void release_kept_inproc_syncs( struct process *process )
+{
+}
+
static int get_obj_inproc_sync( struct object *obj, int *type )
{
return -1;
@@ -411,3 +488,17 @@ DECL_HANDLER(get_inproc_sync_fd)
release_object( obj );
}
+
+DECL_HANDLER(keep_inproc_sync)
+{
+ struct object *obj;
+
+ if (!(obj = get_handle_obj( current->process, req->handle, 0, NULL ))) return;
+ keep_inproc_mutex( current->process, obj, req->serial );
+ release_object( obj );
+}
+
+DECL_HANDLER(release_kept_inproc_sync)
+{
+ release_kept_inproc_mutex( current->process, req->serial );
+}
diff --git a/server/object.h b/server/object.h
index 21f5ea88bd3..90561e393fc 100644
--- a/server/object.h
+++ b/server/object.h
@@ -255,6 +255,7 @@ extern struct inproc_sync *create_inproc_event_sync( int manual, int signaled );
extern struct inproc_sync *create_inproc_semaphore_sync( unsigned int initial, unsigned int max );
extern struct inproc_sync *create_inproc_mutex_sync( thread_id_t owner, unsigned int count );
extern void abandon_inproc_mutexes( thread_id_t owner );
+extern void release_kept_inproc_syncs( struct process *process );
extern void signal_inproc_sync( struct inproc_sync *sync );
extern void reset_inproc_sync( struct inproc_sync *sync );
diff --git a/server/process.c b/server/process.c
index b3992304aaf..7cf792de80a 100644
--- a/server/process.c
+++ b/server/process.c
@@ -811,6 +811,7 @@ static void process_destroy( struct object *obj )
assert( !process->sigkill_timeout ); /* timeout should hold a reference to the process */
close_process_handles( process );
+ release_kept_inproc_syncs( process );
set_process_startup_state( process, STARTUP_ABORTED );
if (process->job)
@@ -1016,6 +1017,7 @@ static void process_killed( struct process *process )
process->desktop = 0;
cancel_terminating_process_asyncs( process );
close_process_handles( process );
+ release_kept_inproc_syncs( process );
if (process->idle_event) release_object( process->idle_event );
process->idle_event = NULL;
assert( !process->console );
diff --git a/server/protocol.def b/server/protocol.def
index 4eb22fb2bb1..ad3a5b836ac 100644
--- a/server/protocol.def
+++ b/server/protocol.def
@@ -4558,3 +4558,19 @@ enum fsync_type
@REPLY
unsigned int status; /* enum hwnd_dmabuf_status */
@END
+
+
+/* The next two requests are appended after every other one so that existing request numbers stay the same. */
+
+/* Keep the in-process sync of a mutex whose handle is about to be closed while another thread of the process
+ * is still using it, until release_kept_inproc_sync with the same serial or the death of the process */
+@REQ(keep_inproc_sync)
+ obj_handle_t handle; /* handle that is about to be closed */
+ unsigned int serial; /* id chosen by the client, unique within the process */
+@END
+
+
+/* Stop keeping an in-process sync that keep_inproc_sync kept: its last user is done */
+@REQ(release_kept_inproc_sync)
+ unsigned int serial; /* serial given to keep_inproc_sync */
+@END
@@ -0,0 +1,122 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Eggleston <[email protected]>
Date: Wed, 30 Sep 2026 13:40:00 -0700
Subject: [PATCH] server: Keep an owned ntsync mutex until its owner releases
it or dies.
934514db7c2 ("server: Keep owned mutex syncs alive until abandoned.")
keeps a server-side mutex alive while a thread owns it, so that the
owner's death abandons it even after the last handle is closed; the end
of kernel32:sync test_mutex() checks that. The in-process mutexes used
with /dev/ntsync have no equivalent: a waiter holds a duplicate of the
ntsync fd, so the kernel object stays alive, but the server object is
destroyed with the last handle and the mutex leaves the list that
abandon_inproc_mutexes() walks. When the owner then dies nothing sends
NTSYNC_IOC_MUTEX_KILL for it, the mutex is never abandoned, and the
waiter blocks forever, where Windows and the server-side mutex return
WAIT_ABANDONED.
The server doesn't see in-process acquisitions, so it can't hold a
reference while the mutex is owned. Instead, keep the fd of a mutex
that is still owned when its object is destroyed, kill it together with
the live mutexes when a thread dies, and close it once it is no longer
owned.
Fixes the two kernel32:sync test_mutex() failures with /dev/ntsync,
which have failed since in-process synchronization was enabled in
7bb835b3482.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=60417
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
server/inproc_sync.c | 50 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 49 insertions(+), 1 deletion(-)
diff --git a/server/inproc_sync.c b/server/inproc_sync.c
index a7020ef8fcd..7199915ac76 100644
--- a/server/inproc_sync.c
+++ b/server/inproc_sync.c
@@ -23,6 +23,7 @@
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
+#include <stdlib.h>
#include "ntstatus.h"
#define WIN32_NO_STATUS
@@ -75,6 +76,37 @@ struct inproc_sync
static struct list inproc_mutexes = LIST_INIT( inproc_mutexes );
+/* Mutexes whose server object was destroyed while a thread still owned them. A client can still hold
+ * a duplicate of the fd (a thread waiting on a handle that has since been closed), and the ntsync
+ * driver abandons a mutex only through NTSYNC_IOC_MUTEX_KILL on that mutex, so the fd is kept until
+ * the owner has released or abandoned it. */
+struct orphan_mutex
+{
+ struct list entry;
+ int fd;
+};
+
+static struct list orphan_mutexes = LIST_INIT( orphan_mutexes );
+
+static int inproc_mutex_owned( int fd )
+{
+ struct ntsync_mutex_args args = {0};
+ return !ioctl( fd, NTSYNC_IOC_MUTEX_READ, &args ) && args.owner;
+}
+
+static void prune_orphan_mutexes(void)
+{
+ struct orphan_mutex *orphan, *next;
+
+ LIST_FOR_EACH_ENTRY_SAFE( orphan, next, &orphan_mutexes, struct orphan_mutex, entry )
+ {
+ if (inproc_mutex_owned( orphan->fd )) continue;
+ list_remove( &orphan->entry );
+ close( orphan->fd );
+ free( orphan );
+ }
+}
+
static void inproc_sync_dump( struct object *obj, int verbose );
static int inproc_sync_signal( struct object *obj, unsigned int access, int signal );
static void inproc_sync_destroy( struct object *obj );
@@ -262,12 +294,25 @@ static void inproc_sync_destroy( struct object *obj )
assert( obj->ops == &inproc_sync_ops );
list_remove( &sync->entry );
if (do_fsync()) fsync_free_shm_idx( sync->fd );
- else close( sync->fd );
+ else if (sync->type == INPROC_SYNC_MUTEX && inproc_mutex_owned( sync->fd ))
+ {
+ struct orphan_mutex *orphan;
+
+ prune_orphan_mutexes();
+ if ((orphan = malloc( sizeof(*orphan) )))
+ {
+ orphan->fd = sync->fd;
+ list_add_tail( &orphan_mutexes, &orphan->entry );
+ }
+ else close( sync->fd );
+ }
+ else close( sync->fd );
}
void abandon_inproc_mutexes( thread_id_t tid )
{
struct inproc_sync *mutex;
+ struct orphan_mutex *orphan;
if (do_fsync())
{
@@ -278,6 +323,9 @@ void abandon_inproc_mutexes( thread_id_t tid )
LIST_FOR_EACH_ENTRY( mutex, &inproc_mutexes, struct inproc_sync, entry )
ioctl( mutex->fd, NTSYNC_IOC_MUTEX_KILL, &tid );
+ LIST_FOR_EACH_ENTRY( orphan, &orphan_mutexes, struct orphan_mutex, entry )
+ ioctl( orphan->fd, NTSYNC_IOC_MUTEX_KILL, &tid );
+ prune_orphan_mutexes();
}
static int get_obj_inproc_sync( struct object *obj, int *type )
@@ -0,0 +1,204 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] win32u: Defer destroying NVIDIA Wayland host instances while
another one is alive.
The NVIDIA proprietary driver creates its Wayland WSI state with the first
Wayland surface of a VkInstance. That state dlopens libwayland-client and
stores wl_display_* entry points in a process-global table. Destroying the
VkInstance dlcloses the library and zeroes that table, even when other
instances still use it. The next surface call on a surviving instance then
calls a NULL wl_display_create_queue from inside the driver.
This reproduces natively without Wine (driver 610.43.02): create instance A
and a Wayland surface, create instance B and a Wayland surface, destroy B,
then vkGetPhysicalDeviceSurfaceFormats2KHR on A's surface crashes. Instances
that never created a Wayland surface do not trigger it, and destroying only
the surface does not either.
The EA app hits this on winewayland: DXVK creates and destroys several
instances during startup while another one stays alive, and the next format
query on a hidden helper window's surface faults, so UNIX_CALL_CHECKED exits
the process with code 3.
Keep the host instance of such an instance alive while another NVIDIA
Wayland instance is alive, and destroy all deferred ones together with the
last one. Creating the first surface of an instance and destroying host
instances are serialized, so the table is never cleared while a live
instance depends on it. A deferred instance costs about 240 kB.
Set WINE_VK_NO_NVIDIA_WAYLAND_INSTANCE_DEFER=1 to disable the workaround.
Fixes: https://github.com/GloriousEggroll/proton-ge-custom/issues/881
---
--- a/dlls/win32u/vulkan.c
+++ b/dlls/win32u/vulkan.c
@@ -148,6 +148,9 @@
struct rb_tree objects;
pthread_rwlock_t objects_lock;
+
+ BOOL nvidia_wayland; /* host instance uses Wayland WSI with the NVIDIA proprietary driver */
+ BOOL nvidia_wayland_surface; /* a host Wayland surface was created on this instance */
};
static struct instance *instance_from_handle( VkInstance handle )
@@ -156,6 +159,104 @@
return CONTAINING_RECORD( object, struct instance, obj );
}
+/* The NVIDIA proprietary driver creates its Wayland WSI state with the first
+ * Wayland surface of a VkInstance. That state loads libwayland-client into a
+ * process-global function table, and destroying the VkInstance dlcloses the
+ * library and clears the table, even while other instances still use it.
+ * Their next surface call then jumps to a NULL wl_display_create_queue.
+ *
+ * Defer host destruction of such instances while another one is alive, and
+ * destroy them all once the last one goes away. Every host instance destroy
+ * and every first surface creation runs under nvidia_wayland_lock, so the
+ * table is never cleared while it is filled for a live instance. */
+struct deferred_host_instance
+{
+ struct list entry;
+ VkInstance host_instance;
+ PFN_vkDestroyInstance p_vkDestroyInstance;
+};
+
+static pthread_mutex_t nvidia_wayland_lock = PTHREAD_MUTEX_INITIALIZER;
+static struct list nvidia_wayland_deferred = LIST_INIT( nvidia_wayland_deferred );
+static unsigned int nvidia_wayland_live;
+
+static BOOL instance_uses_nvidia_wayland( struct vulkan_instance *instance )
+{
+ PFN_vkGetPhysicalDeviceProperties2 get_properties2;
+ const char *env;
+ unsigned int i;
+
+ if (!instance->extensions.has_VK_KHR_wayland_surface) return FALSE;
+ if ((env = getenv( "WINE_VK_NO_NVIDIA_WAYLAND_INSTANCE_DEFER" )) && atoi( env )) return FALSE;
+
+ if (instance->extensions.has_VK_KHR_get_physical_device_properties2)
+ get_properties2 = instance->p_vkGetPhysicalDeviceProperties2KHR;
+ else if (instance->api_version >= VK_API_VERSION_1_1)
+ get_properties2 = instance->p_vkGetPhysicalDeviceProperties2;
+ else
+ get_properties2 = NULL;
+
+ for (i = 0; i < instance->physical_device_count; i++)
+ {
+ VkPhysicalDevice host_physical_device = instance->physical_devices[i].host.physical_device;
+ VkPhysicalDeviceDriverProperties driver_props = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
+ VkPhysicalDeviceProperties2 props = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver_props};
+
+ if (get_properties2 && instance->physical_devices[i].api_version >= VK_API_VERSION_1_2)
+ {
+ get_properties2( host_physical_device, &props );
+ if (driver_props.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY) return TRUE;
+ }
+ else
+ {
+ instance->p_vkGetPhysicalDeviceProperties( host_physical_device, &props.properties );
+ if (props.properties.vendorID == 0x10de) return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void destroy_host_instance( struct instance *instance )
+{
+ struct deferred_host_instance *deferred, *next;
+
+ if (!instance->nvidia_wayland)
+ {
+ instance->obj.p_vkDestroyInstance( instance->obj.host.instance, NULL /* allocator */ );
+ return;
+ }
+
+ pthread_mutex_lock( &nvidia_wayland_lock );
+ if (instance->nvidia_wayland_surface && --nvidia_wayland_live)
+ {
+ if ((deferred = malloc( sizeof(*deferred) )))
+ {
+ TRACE( "Deferring destruction of host instance %p, %u NVIDIA Wayland instances alive.\n",
+ instance->obj.host.instance, nvidia_wayland_live );
+ deferred->host_instance = instance->obj.host.instance;
+ deferred->p_vkDestroyInstance = instance->obj.p_vkDestroyInstance;
+ list_add_tail( &nvidia_wayland_deferred, &deferred->entry );
+ pthread_mutex_unlock( &nvidia_wayland_lock );
+ return;
+ }
+ ERR( "Failed to defer destruction of host instance %p.\n", instance->obj.host.instance );
+ }
+
+ instance->obj.p_vkDestroyInstance( instance->obj.host.instance, NULL /* allocator */ );
+ if (!nvidia_wayland_live)
+ {
+ LIST_FOR_EACH_ENTRY_SAFE( deferred, next, &nvidia_wayland_deferred, struct deferred_host_instance, entry )
+ {
+ TRACE( "Destroying deferred host instance %p.\n", deferred->host_instance );
+ list_remove( &deferred->entry );
+ deferred->p_vkDestroyInstance( deferred->host_instance, NULL /* allocator */ );
+ free( deferred );
+ }
+ }
+ pthread_mutex_unlock( &nvidia_wayland_lock );
+}
+
struct device_memory
{
struct vulkan_device_memory obj;
@@ -1451,6 +1552,8 @@
*/
if ((res = init_physical_devices( &instance->obj, physical_devices ))) goto failed;
+ instance->nvidia_wayland = instance_uses_nvidia_wayland( &instance->obj );
+
TRACE( "Created instance %p, host_instance %p.\n", instance, instance->obj.host.instance );
for (i = 0; i < instance->obj.physical_device_count; i++)
{
@@ -1478,7 +1581,7 @@
if (!instance) return;
- instance->obj.p_vkDestroyInstance( instance->obj.host.instance, NULL /* allocator */ );
+ destroy_host_instance( instance );
for (int i = 0; i < instance->obj.physical_device_count; i++)
vulkan_instance_remove_object( &instance->obj, &instance->obj.physical_devices[i].obj );
vulkan_instance_remove_object( &instance->obj, &instance->obj.obj );
@@ -2532,6 +2635,8 @@
const VkAllocationCallbacks *allocator, VkSurfaceKHR *ret )
{
struct vulkan_instance *instance = vulkan_instance_from_handle( client_instance );
+ struct instance *win32u_instance = CONTAINING_RECORD( instance, struct instance, obj );
+ BOOL nvidia_wayland_locked = FALSE;
VkSurfaceKHR host_surface;
struct surface *surface;
HWND dummy = NULL;
@@ -2553,13 +2658,27 @@
surface->hwnd = dummy;
}
+ if (win32u_instance->nvidia_wayland)
+ {
+ pthread_mutex_lock( &nvidia_wayland_lock );
+ if (win32u_instance->nvidia_wayland_surface) pthread_mutex_unlock( &nvidia_wayland_lock );
+ else nvidia_wayland_locked = TRUE;
+ }
+
if ((res = driver_funcs->p_vulkan_surface_create( surface->hwnd, fshack_enabled, instance,
&host_surface, &surface->client )))
{
+ if (nvidia_wayland_locked) pthread_mutex_unlock( &nvidia_wayland_lock );
if (dummy) NtUserDestroyWindow( dummy );
free( surface );
return res;
}
+ if (nvidia_wayland_locked)
+ {
+ win32u_instance->nvidia_wayland_surface = TRUE;
+ nvidia_wayland_live++;
+ pthread_mutex_unlock( &nvidia_wayland_lock );
+ }
add_window_client_surface( surface->hwnd, surface->client );
vulkan_object_init( &surface->obj.obj, host_surface );
@@ -0,0 +1,101 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] win32u: Prefer the native HDR colorspace when the host supports it.
Since KWin 6.8 advertises windows_bt2100/windows_scrgb, winewayland maps
HDR10_ST2084 and scRGB swapchains to PASS_THROUGH and attaches a Windows
image description to the surface. Host Vulkan layers such as MangoHud and the
Steam overlay then only see PASS_THROUGH and draw SDR colors into the PQ
buffer, which looks oversaturated.
Keep the requested HDR colorspace when the host lists it for the swapchain
format, and use the driver mapping only as a fallback. This is the path
winewayland already took on compositors without the Windows descriptions.
WINE_WAYLAND_HDR_PASSTHROUGH=1 restores the previous preference.
https://github.com/GloriousEggroll/proton-ge-custom/issues/898
https://github.com/GloriousEggroll/proton-ge-custom/issues/916
---
--- a/dlls/win32u/vulkan.c
+++ b/dlls/win32u/vulkan.c
@@ -5127,6 +5127,41 @@
return !wcsicmp( name, doom_eternalW ) || !wcsicmp( name, doom_dark_agesW );
}
+/* Host layers such as MangoHud and the Steam overlay only see the host swapchain
+ * colorspace, and draw SDR colors into a pass-through HDR swapchain. Keep a native
+ * HDR10/scRGB colorspace when the host can present it, and only fall back to the
+ * driver's colorspace mapping when it can't. WINE_WAYLAND_HDR_PASSTHROUGH=1
+ * restores the previous preference for the mapping. */
+static BOOL prefer_native_hdr_colorspace( struct vulkan_physical_device *physical_device, struct surface *surface,
+ VkFormat format, VkColorSpaceKHR color_space )
+{
+ struct vulkan_instance *instance = physical_device->instance;
+ VkSurfaceFormatKHR *formats;
+ uint32_t i, count = 0;
+ BOOL found = FALSE;
+ const char *env;
+
+ if (color_space != VK_COLOR_SPACE_HDR10_ST2084_EXT && color_space != VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT)
+ return FALSE;
+ if ((env = getenv( "WINE_WAYLAND_HDR_PASSTHROUGH" )) && atoi( env )) return FALSE;
+
+ if (instance->p_vkGetPhysicalDeviceSurfaceFormatsKHR( physical_device->host.physical_device,
+ surface->obj.host.surface, &count, NULL ) || !count)
+ return FALSE;
+ if (!(formats = calloc( count, sizeof(*formats) ))) return FALSE;
+
+ if (!instance->p_vkGetPhysicalDeviceSurfaceFormatsKHR( physical_device->host.physical_device,
+ surface->obj.host.surface, &count, formats ))
+ {
+ for (i = 0; i < count && !found; i++)
+ found = formats[i].format == format && formats[i].colorSpace == color_space;
+ }
+ free( formats );
+
+ TRACE( "format %u colorspace %u native %u\n", format, color_space, found );
+ return found;
+}
+
static VkResult win32u_vkCreateSwapchainKHR( VkDevice client_device, const VkSwapchainCreateInfoKHR *create_info,
const VkAllocationCallbacks *allocator, VkSwapchainKHR *ret )
{
@@ -5142,7 +5177,7 @@
VkSwapchainKHR host_swapchain;
uint32_t format_count = 0;
VkSurfaceFormatKHR *formats;
- BOOL presentation_owner;
+ BOOL presentation_owner, native_hdr;
RECT client_rect;
VkResult res;
BOOL lite;
@@ -5163,7 +5198,6 @@
res = instance->p_vkGetPhysicalDeviceSurfaceCapabilitiesKHR( physical_device->host.physical_device, surface->obj.host.surface, &capabilities );
if (res) return res;
- create_info_host.imageColorSpace = driver_funcs->p_vulkan_map_colorspace( create_info_host.imageColorSpace, surface->client );
driver_funcs->p_vulkan_surface_set_alpha( create_info_host.compositeAlpha, surface->client );
create_info_host.imageExtent.width = max( create_info_host.imageExtent.width, capabilities.minImageExtent.width );
create_info_host.imageExtent.height = max( create_info_host.imageExtent.height, capabilities.minImageExtent.height );
@@ -5221,6 +5255,13 @@
create_info_host.imageFormat );
}
+ native_hdr = prefer_native_hdr_colorspace( physical_device, surface, create_info_host.imageFormat,
+ create_info_host.imageColorSpace );
+ if (native_hdr) /* detach any color description attached for a previous swapchain */
+ driver_funcs->p_vulkan_map_colorspace( VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, surface->client );
+ else
+ create_info_host.imageColorSpace = driver_funcs->p_vulkan_map_colorspace( create_info_host.imageColorSpace, surface->client );
+
/* check if the new colorspace works with the provided format */
if (create_info_host.imageColorSpace != create_info->imageColorSpace)
{
@@ -5263,7 +5304,7 @@
free(formats);
}
- else driver_funcs->p_vulkan_map_colorspace( create_info->imageColorSpace, surface->client );
+ else if (!native_hdr) driver_funcs->p_vulkan_map_colorspace( create_info->imageColorSpace, surface->client );
InterlockedIncrement( &surface->client->busy_ref );
client_surface_update( surface->client );
@@ -0,0 +1,174 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Tue, 20 Oct 2026 20:00:00 -0600
Subject: [PATCH] win32u: Release swapchain images presented to hidden windows.
A window that isn't visible is never shown, and on Windows the
presentation engine simply discards what is presented to it. The host
present is not as forgiving on Wayland: the client surface of a window
whose toplevel was never mapped has no role, so the compositor never
sends the frame callbacks a FIFO present waits for. NVIDIA's WSI then
blocks in vkQueuePresentKHR forever (Mesa times out instead).
RuneScape's Vulkan client renders its loading frames into a child window
before it shows the main window. With MSAA enabled it recreates its
swapchain during loading, and the second present after that never
returns on winewayland with NVIDIA, so the game never appears.
When swapchainMaintenance1 is available, enable it on the host device,
and for host swapchains of windows that are not visible, skip the host
present: consume the present waits, wait for the device, release the
image with vkReleaseSwapchainImagesEXT() and signal any present fence.
Visible windows and managed (cross-process) swapchains are unchanged.
https://github.com/GloriousEggroll/proton-ge-custom/issues/837
---
diff --git a/include/wine/vulkan_driver.h b/include/wine/vulkan_driver.h
--- a/include/wine/vulkan_driver.h
+++ b/include/wine/vulkan_driver.h
@@ -233,6 +233,7 @@
struct vulkan_queue *queues;
VkQueueFamilyProperties *queue_props;
BOOL low_latency_enabled;
+ BOOL release_swapchain_images; /* swapchainMaintenance1 enabled for presents to hidden windows */
pthread_t signaller_thread;
pthread_mutex_t signaller_mutex;
diff --git a/dlls/win32u/vulkan.c b/dlls/win32u/vulkan.c
--- a/dlls/win32u/vulkan.c
+++ b/dlls/win32u/vulkan.c
@@ -1533,6 +1533,20 @@
return maintenance.swapchainMaintenance1;
}
+static BOOL host_supports_swapchain_maintenance1( struct vulkan_physical_device *physical_device )
+{
+ struct vulkan_instance *instance = physical_device->instance;
+ VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenance =
+ {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT};
+ VkPhysicalDeviceFeatures2 features = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &maintenance};
+
+ if (!physical_device->extensions.has_VK_EXT_swapchain_maintenance1 || !instance->p_vkGetPhysicalDeviceFeatures2KHR)
+ return FALSE;
+
+ instance->p_vkGetPhysicalDeviceFeatures2KHR( physical_device->host.physical_device, &features );
+ return maintenance.swapchainMaintenance1;
+}
+
static VkResult convert_device_create_info( struct vulkan_physical_device *physical_device, VkDeviceCreateInfo *info,
struct mempool *pool, struct vulkan_device *device )
{
@@ -1603,6 +1617,24 @@
maintenance->swapchainMaintenance1 = VK_TRUE;
}
+ /* Presents to hidden windows release their images instead of reaching the
+ * host compositor, see win32u_vkQueuePresentKHR. That needs the feature. */
+ if (device->extensions.has_VK_EXT_swapchain_maintenance1 && host_supports_swapchain_maintenance1( physical_device ))
+ {
+ VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT *maintenance;
+
+ if (!(maintenance = (void *)find_next_struct( info->pNext,
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT )))
+ {
+ if (!(maintenance = mem_alloc( pool, sizeof(*maintenance) ))) return VK_ERROR_OUT_OF_HOST_MEMORY;
+ maintenance->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT;
+ maintenance->pNext = (void *)info->pNext;
+ info->pNext = maintenance;
+ }
+ maintenance->swapchainMaintenance1 = VK_TRUE;
+ device->release_swapchain_images = TRUE;
+ }
+
/* Force-enable the extensions the cross-process producer needs so we can
* interpose a managed swapchain even when the app did not request them.
* Only meaningful for swapchain devices. */
@@ -6207,10 +6239,11 @@
const VkSwapchainKHR *client_swapchains;
VkSwapchainKHR *swapchains;
uint32_t host_indices_buffer[16], *host_indices = host_indices_buffer;
+ uint32_t hidden_indices_buffer[16], *hidden_indices = hidden_indices_buffer;
VkSemaphore *host_wait_semaphores = NULL;
const VkSwapchainPresentFenceInfoKHR *present_fence_info = NULL;
struct wine_managed_swapchain *first_managed = NULL;
- uint32_t host_count = 0;
+ uint32_t host_count = 0, hidden_count = 0;
BOOL skip_managed = FALSE;
BOOL managed_present_waits_consumed = FALSE;
VkCommandBuffer *blit_cmds;
@@ -6224,6 +6257,8 @@
if (!(blit_cmds = mem_alloc( &pool, present_info->swapchainCount * sizeof(blit_cmds) ))) goto failed;
if (present_info->swapchainCount > ARRAY_SIZE(host_indices_buffer) &&
!(host_indices = mem_alloc( &pool, present_info->swapchainCount * sizeof(*host_indices) ))) goto failed;
+ if (present_info->swapchainCount > ARRAY_SIZE(hidden_indices_buffer) &&
+ !(hidden_indices = mem_alloc( &pool, present_info->swapchainCount * sizeof(*hidden_indices) ))) goto failed;
for (uint32_t i = 0; i < present_info->swapchainCount; ++i)
{
@@ -6274,6 +6309,16 @@
if (!first_managed) first_managed = swapchain->managed;
continue;
}
+ /* A hidden window is never shown, and on Wayland its client surface has no
+ * role, so the compositor never sends the frame callbacks a FIFO present
+ * waits for (NVIDIA blocks forever). Release the image instead, like the
+ * presentation engine discards it on Windows. */
+ if (device->release_swapchain_images && device->p_vkReleaseSwapchainImagesEXT &&
+ !is_window_visible( swapchain->surface->hwnd ))
+ {
+ hidden_indices[hidden_count++] = i;
+ continue;
+ }
swapchains[host_count] = swapchain->obj.host.swapchain;
host_indices[host_count] = i;
host_count++;
@@ -6391,6 +6436,49 @@
else managed_present_waits_consumed = TRUE;
}
+ /* Release images presented to hidden windows once the GPU is done with them. */
+ if (hidden_count)
+ {
+ VkResult hidden_res = res < VK_SUCCESS ? res : VK_SUCCESS;
+
+ if (hidden_res == VK_SUCCESS)
+ {
+ pthread_mutex_lock( &producer_device_lock );
+ hidden_res = device->p_vkDeviceWaitIdle( device->host.device );
+ pthread_mutex_unlock( &producer_device_lock );
+ }
+
+ for (uint32_t k = 0; k < hidden_count; k++)
+ {
+ uint32_t i = hidden_indices[k];
+ struct swapchain *swapchain = swapchain_from_handle( client_swapchains[i] );
+ VkReleaseSwapchainImagesInfoKHR release =
+ {
+ .sType = VK_STRUCTURE_TYPE_RELEASE_SWAPCHAIN_IMAGES_INFO_KHR,
+ .swapchain = swapchain->obj.host.swapchain,
+ .imageIndexCount = 1,
+ .pImageIndices = &present_info->pImageIndices[i],
+ };
+ VkResult image_res = hidden_res;
+
+ TRACE( "window %p is hidden, releasing image %u of swapchain %p instead of presenting\n",
+ swapchain->surface->hwnd, present_info->pImageIndices[i], swapchain );
+
+ if (image_res == VK_SUCCESS)
+ {
+ pthread_mutex_lock( &producer_device_lock );
+ image_res = device->p_vkReleaseSwapchainImagesEXT( device->host.device, &release );
+ /* No host present will signal this fence. */
+ if (present_fence_info && i < present_fence_info->swapchainCount && present_fence_info->pFences[i])
+ device->p_vkQueueSubmit( queue->host.queue, 0, NULL, present_fence_info->pFences[i] );
+ pthread_mutex_unlock( &producer_device_lock );
+ }
+
+ if (present_info->pResults) present_info->pResults[i] = image_res;
+ if (image_res < VK_SUCCESS && res >= VK_SUCCESS) res = image_res;
+ }
+ }
+
/* Present each managed swapchain after the present waits are consumed. */
for (uint32_t i = 0; i < present_info->swapchainCount; i++)
{
@@ -0,0 +1,94 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] win32u: Report B8G8R8A8_UNORM as the first surface format.
Windows drivers list VK_FORMAT_B8G8R8A8_UNORM with SRGB_NONLINEAR first.
Host drivers use their own order, which also differs between X11 and
Wayland. NVIDIA on winewayland reports R8G8B8A8_SRGB first, and RADV does
the same on XWayland.
RuneScape creates its swapchain with the first reported format and writes
gamma-encoded colors, so an _SRGB format applies the transfer function
twice and the game looks washed out. It only looks correct when the host
happens to list a UNORM format first.
Move B8G8R8A8_UNORM/SRGB_NONLINEAR to the front of complete format lists to
match Windows. Keep the host order for VK_INCOMPLETE results and when an
application chains per-format output structures.
Fixes: https://github.com/GloriousEggroll/proton-ge-custom/issues/837
---
diff --git a/dlls/win32u/vulkan.c b/dlls/win32u/vulkan.c
--- a/dlls/win32u/vulkan.c
+++ b/dlls/win32u/vulkan.c
@@ -2827,15 +2827,55 @@
get_physical_device_properties2( physical_device, properties2, physical_device->instance->p_vkGetPhysicalDeviceProperties2KHR );
}
+/* Windows drivers report B8G8R8A8_UNORM first, and some applications such as
+ * RuneScape blindly use the first format. Host drivers order the list
+ * differently between X11 and Wayland, sometimes with an _SRGB format first,
+ * so move B8G8R8A8_UNORM to the front to match Windows. */
+static BOOL is_windows_default_surface_format( const VkSurfaceFormatKHR *format )
+{
+ return format->format == VK_FORMAT_B8G8R8A8_UNORM && format->colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
+}
+
+static void sort_surface_formats( VkSurfaceFormatKHR *formats, uint32_t count )
+{
+ VkSurfaceFormatKHR format;
+ uint32_t i;
+
+ for (i = 1; i < count; i++) if (is_windows_default_surface_format( &formats[i] )) break;
+ if (i >= count) return;
+
+ format = formats[i];
+ memmove( formats + 1, formats, i * sizeof(*formats) );
+ formats[0] = format;
+}
+
+static void sort_surface_formats2( VkSurfaceFormat2KHR *formats, uint32_t count )
+{
+ VkSurfaceFormat2KHR format;
+ uint32_t i;
+
+ /* extension structs are filled per entry, keep the host order if any are chained */
+ for (i = 0; i < count; i++) if (formats[i].pNext) return;
+ for (i = 1; i < count; i++) if (is_windows_default_surface_format( &formats[i].surfaceFormat )) break;
+ if (i >= count) return;
+
+ format = formats[i];
+ memmove( formats + 1, formats, i * sizeof(*formats) );
+ formats[0] = format;
+}
+
static VkResult win32u_vkGetPhysicalDeviceSurfaceFormatsKHR( VkPhysicalDevice client_physical_device, VkSurfaceKHR client_surface,
uint32_t *format_count, VkSurfaceFormatKHR *formats )
{
struct vulkan_physical_device *physical_device = vulkan_physical_device_from_handle( client_physical_device );
struct surface *surface = surface_from_handle( client_surface );
struct vulkan_instance *instance = physical_device->instance;
+ VkResult res;
- return instance->p_vkGetPhysicalDeviceSurfaceFormatsKHR( physical_device->host.physical_device,
- surface->obj.host.surface, format_count, formats );
+ res = instance->p_vkGetPhysicalDeviceSurfaceFormatsKHR( physical_device->host.physical_device,
+ surface->obj.host.surface, format_count, formats );
+ if (res == VK_SUCCESS && formats) sort_surface_formats( formats, *format_count );
+ return res;
}
static VkResult win32u_vkGetPhysicalDeviceSurfaceFormats2KHR( VkPhysicalDevice client_physical_device, const VkPhysicalDeviceSurfaceInfo2KHR *surface_info,
@@ -2868,8 +2908,10 @@
surface_info_host.surface = surface->obj.host.surface;
- return instance->p_vkGetPhysicalDeviceSurfaceFormats2KHR( physical_device->host.physical_device,
- &surface_info_host, format_count, formats );
+ res = instance->p_vkGetPhysicalDeviceSurfaceFormats2KHR( physical_device->host.physical_device,
+ &surface_info_host, format_count, formats );
+ if (res == VK_SUCCESS && formats) sort_surface_formats2( formats, *format_count );
+ return res;
}
static VkBool32 win32u_vkGetPhysicalDeviceWin32PresentationSupportKHR( VkPhysicalDevice client_physical_device, uint32_t queue )
@@ -0,0 +1,390 @@
From: GloriousEggroll <[email protected]>
Date: Mon, 2 Nov 2026 12:00:00 -0700
Subject: [PATCH] winewayland: Animate animated cursors.
winewayland only read the first frame of a cursor, so game-supplied
animated cursors (.ani) were static, while winex11 hands every frame and
its delay to Xcursor.
Render every step of an animated cursor into its own buffer when the
cursor is set, and have a helper thread attach them to the cursor surface
in turn, at each frame's delay. The thread only uses the prerendered
buffers and Wayland objects, under the pointer mutex. It idles while the
pointer is outside our windows, while a system cursor shape is in use,
and for static cursors.
https://github.com/GloriousEggroll/proton-ge-custom/issues/919
---
--- a/dlls/winewayland.drv/waylanddrv.h
+++ b/dlls/winewayland.drv/waylanddrv.h
@@ -136,12 +136,23 @@
unsigned char keystate[0x300];
};
+struct wayland_cursor_frame
+{
+ struct wayland_shm_buffer *shm_buffer;
+ int hotspot_x, hotspot_y;
+ UINT delay_ms;
+};
+
struct wayland_cursor
{
struct wayland_shm_buffer *shm_buffer;
struct wl_surface *wl_surface;
struct wp_viewport *wp_viewport;
int hotspot_x, hotspot_y;
+ double scale;
+ /* Frames of an animated cursor, NULL for a static one. */
+ struct wayland_cursor_frame *frames;
+ UINT num_frames, current_frame;
};
enum wayland_pointer_frame_flags
--- a/dlls/winewayland.drv/wayland_pointer.c
+++ b/dlls/winewayland.drv/wayland_pointer.c
@@ -24,9 +24,12 @@
#include "config.h"
+#include <errno.h>
#include <linux/input.h>
#undef SW_MAX /* Also defined in winuser.rh */
#include <math.h>
+#include <pthread.h>
+#include <signal.h>
#include <stdlib.h>
#include <time.h>
@@ -705,72 +708,143 @@
return TRUE;
}
-static void wayland_pointer_update_cursor_buffer(HCURSOR hcursor, double scale)
+/* Create the buffer and the scaled hotspot for one cursor image. */
+static struct wayland_shm_buffer *create_cursor_image_buffer(HCURSOR hcursor, double scale,
+ int *hotspot_x, int *hotspot_y)
{
- struct wayland_cursor *cursor = &process_wayland.pointer.cursor;
+ struct wayland_shm_buffer *shm_buffer = NULL;
ICONINFOEXW info = {0};
-
- if (!hcursor) goto clear_cursor;
-
- /* Create a new buffer for the specified cursor. */
- if (cursor->shm_buffer)
- {
- wayland_shm_buffer_unref(cursor->shm_buffer);
- cursor->shm_buffer = NULL;
- }
+ int x, y;
if (!get_icon_info(hcursor, &info))
{
ERR("Failed to get icon info for cursor=%p\n", hcursor);
- goto clear_cursor;
+ return NULL;
}
if (info.hbmColor)
{
HDC hdc = NtGdiCreateCompatibleDC(0);
- cursor->shm_buffer =
- wayland_shm_buffer_from_color_bitmaps(hdc, info.hbmColor, info.hbmMask, FALSE);
+ shm_buffer = wayland_shm_buffer_from_color_bitmaps(hdc, info.hbmColor, info.hbmMask, FALSE);
NtGdiDeleteObjectApp(hdc);
}
else
{
- cursor->shm_buffer = create_mono_cursor_buffer(info.hbmMask);
+ shm_buffer = create_mono_cursor_buffer(info.hbmMask);
}
if (info.hbmColor) NtGdiDeleteObjectApp(info.hbmColor);
if (info.hbmMask) NtGdiDeleteObjectApp(info.hbmMask);
- cursor->hotspot_x = info.xHotspot;
- cursor->hotspot_y = info.yHotspot;
-
- if (!cursor->shm_buffer)
+ if (!shm_buffer)
{
ERR("Failed to create shm_buffer for cursor=%p\n", hcursor);
- goto clear_cursor;
+ return NULL;
}
- if (cursor_buffer_is_transparent(cursor->shm_buffer))
- goto clear_cursor;
+ x = info.xHotspot;
+ y = info.yHotspot;
/* Make sure the hotspot is valid. */
- if (cursor->hotspot_x >= cursor->shm_buffer->width ||
- cursor->hotspot_y >= cursor->shm_buffer->height)
+ if (x >= shm_buffer->width || y >= shm_buffer->height)
{
- cursor->hotspot_x = cursor->shm_buffer->width / 2;
- cursor->hotspot_y = cursor->shm_buffer->height / 2;
+ x = shm_buffer->width / 2;
+ y = shm_buffer->height / 2;
}
- cursor->hotspot_x = round(cursor->hotspot_x / scale);
- cursor->hotspot_y = round(cursor->hotspot_y / scale);
+ *hotspot_x = round(x / scale);
+ *hotspot_y = round(y / scale);
- return;
+ return shm_buffer;
+}
-clear_cursor:
+static void wayland_cursor_free_frames(struct wayland_cursor *cursor)
+{
+ UINT i;
+
+ for (i = 0; i < cursor->num_frames; i++)
+ if (cursor->frames[i].shm_buffer)
+ wayland_shm_buffer_unref(cursor->frames[i].shm_buffer);
+ free(cursor->frames);
+ cursor->frames = NULL;
+ cursor->num_frames = 0;
+ cursor->current_frame = 0;
+}
+
+/* Render every step of an animated cursor. Returns FALSE for a static
+ * cursor, or if a frame can't be rendered, so that the caller falls back to
+ * showing a single image. */
+static BOOL wayland_cursor_create_frames(struct wayland_cursor *cursor,
+ HCURSOR hcursor, double scale)
+{
+ struct wayland_cursor_frame *frames;
+ DWORD delay_jiffies, num_steps;
+ UINT i;
+
+ if (!NtUserGetCursorFrameInfo(hcursor, 0, &delay_jiffies, &num_steps)) return FALSE;
+ /* win32u reports ~0 steps for an animation with a single step. */
+ if (num_steps <= 1 || num_steps == ~0u) return FALSE;
+ if (num_steps > 256) num_steps = 256;
+
+ if (!(frames = calloc(num_steps, sizeof(*frames)))) return FALSE;
+
+ for (i = 0; i < num_steps; i++)
+ {
+ HCURSOR frame = NtUserGetCursorFrameInfo(hcursor, i, &delay_jiffies, &num_steps);
+
+ if (!frame ||
+ !(frames[i].shm_buffer = create_cursor_image_buffer(frame, scale,
+ &frames[i].hotspot_x,
+ &frames[i].hotspot_y)))
+ {
+ WARN("Failed to create frame %u of cursor=%p, not animating\n", i, hcursor);
+ cursor->frames = frames;
+ cursor->num_frames = i;
+ wayland_cursor_free_frames(cursor);
+ return FALSE;
+ }
+
+ /* The delay is in jiffies (1/60 s). */
+ frames[i].delay_ms = delay_jiffies ? max(delay_jiffies * 100 / 6, 10) : 100;
+ }
+
+ TRACE("cursor=%p has %u frames\n", hcursor, i);
+
+ cursor->frames = frames;
+ cursor->num_frames = i;
+ cursor->current_frame = 0;
+ cursor->shm_buffer = frames[0].shm_buffer;
+ wayland_shm_buffer_ref(cursor->shm_buffer);
+ cursor->hotspot_x = frames[0].hotspot_x;
+ cursor->hotspot_y = frames[0].hotspot_y;
+ return TRUE;
+}
+
+static void wayland_pointer_update_cursor_buffer(HCURSOR hcursor, double scale)
+{
+ struct wayland_cursor *cursor = &process_wayland.pointer.cursor;
+
+ /* Create new buffers for the specified cursor. */
+ wayland_cursor_free_frames(cursor);
if (cursor->shm_buffer)
{
wayland_shm_buffer_unref(cursor->shm_buffer);
cursor->shm_buffer = NULL;
}
+
+ if (!hcursor) return;
+
+ if (wayland_cursor_create_frames(cursor, hcursor, scale)) return;
+
+ cursor->shm_buffer = create_cursor_image_buffer(hcursor, scale,
+ &cursor->hotspot_x,
+ &cursor->hotspot_y);
+
+ if (cursor->shm_buffer && cursor_buffer_is_transparent(cursor->shm_buffer))
+ {
+ wayland_shm_buffer_unref(cursor->shm_buffer);
+ cursor->shm_buffer = NULL;
+ }
}
static void wayland_pointer_clear_cursor_surface(void)
@@ -792,6 +866,7 @@
wayland_shm_buffer_unref(cursor->shm_buffer);
cursor->shm_buffer = NULL;
}
+ wayland_cursor_free_frames(cursor);
}
static void wayland_pointer_update_cursor_surface(double scale)
@@ -800,6 +875,8 @@
if (!cursor->shm_buffer) goto clear_cursor;
+ cursor->scale = scale;
+
if (!cursor->wl_surface)
{
cursor->wl_surface =
@@ -845,6 +922,132 @@
wayland_pointer_clear_cursor_surface();
}
+/* Animated cursors: the frames are rendered when the cursor is set, and this
+ * thread attaches them to the cursor surface in turn. It only uses Wayland
+ * objects and the prerendered buffers, all under the pointer mutex. */
+static pthread_once_t cursor_animation_once = PTHREAD_ONCE_INIT;
+static pthread_cond_t cursor_animation_cond;
+static BOOL cursor_animation_ok;
+static struct timespec cursor_animation_deadline;
+
+static void timespec_add_ms(struct timespec *ts, UINT ms)
+{
+ ts->tv_sec += ms / 1000;
+ ts->tv_nsec += (long)(ms % 1000) * 1000000;
+ if (ts->tv_nsec >= 1000000000)
+ {
+ ts->tv_sec++;
+ ts->tv_nsec -= 1000000000;
+ }
+}
+
+static BOOL cursor_animation_active(void)
+{
+ struct wayland_pointer *pointer = &process_wayland.pointer;
+
+ return pointer->cursor.num_frames > 1 && pointer->cursor.wl_surface &&
+ pointer->wl_pointer && pointer->focused_hwnd;
+}
+
+static void *cursor_animation_thread(void *arg)
+{
+ struct wayland_pointer *pointer = &process_wayland.pointer;
+ struct wayland_cursor *cursor = &pointer->cursor;
+ struct wayland_cursor_frame *frame;
+ struct timespec now;
+ sigset_t sigset;
+ UINT delay_ms;
+
+ /* Leave signals to the Wine threads. */
+ sigfillset(&sigset);
+ pthread_sigmask(SIG_BLOCK, &sigset, NULL);
+
+ pthread_mutex_lock(&pointer->mutex);
+ for (;;)
+ {
+ if (!cursor_animation_active())
+ {
+ pthread_cond_wait(&cursor_animation_cond, &pointer->mutex);
+ continue;
+ }
+
+ /* Woken early when the cursor changed: start over with the new state. */
+ if (pthread_cond_timedwait(&cursor_animation_cond, &pointer->mutex,
+ &cursor_animation_deadline) != ETIMEDOUT)
+ continue;
+ if (!cursor_animation_active()) continue;
+
+ cursor->current_frame = (cursor->current_frame + 1) % cursor->num_frames;
+ frame = &cursor->frames[cursor->current_frame];
+ /* Updating the surface frees the frames if it fails. */
+ delay_ms = frame->delay_ms;
+
+ wayland_shm_buffer_unref(cursor->shm_buffer);
+ cursor->shm_buffer = frame->shm_buffer;
+ wayland_shm_buffer_ref(cursor->shm_buffer);
+
+ if (frame->hotspot_x != cursor->hotspot_x || frame->hotspot_y != cursor->hotspot_y)
+ {
+ cursor->hotspot_x = frame->hotspot_x;
+ cursor->hotspot_y = frame->hotspot_y;
+ wl_pointer_set_cursor(pointer->wl_pointer, pointer->enter_serial,
+ cursor->wl_surface, cursor->hotspot_x,
+ cursor->hotspot_y);
+ }
+ wayland_pointer_update_cursor_surface(cursor->scale);
+ wl_display_flush(process_wayland.wl_display);
+
+ /* Don't try to catch up on frames we were too late for. */
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ timespec_add_ms(&cursor_animation_deadline, delay_ms);
+ if (cursor_animation_deadline.tv_sec < now.tv_sec ||
+ (cursor_animation_deadline.tv_sec == now.tv_sec &&
+ cursor_animation_deadline.tv_nsec < now.tv_nsec))
+ {
+ cursor_animation_deadline = now;
+ timespec_add_ms(&cursor_animation_deadline, delay_ms);
+ }
+ }
+
+ return NULL;
+}
+
+static void cursor_animation_init(void)
+{
+ pthread_condattr_t attr;
+ pthread_attr_t thread_attr;
+ pthread_t thread;
+
+ pthread_condattr_init(&attr);
+ pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
+ pthread_cond_init(&cursor_animation_cond, &attr);
+ pthread_condattr_destroy(&attr);
+
+ pthread_attr_init(&thread_attr);
+ pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
+ if (!pthread_create(&thread, &thread_attr, cursor_animation_thread, NULL))
+ cursor_animation_ok = TRUE;
+ else
+ ERR("Failed to create the cursor animation thread\n");
+ pthread_attr_destroy(&thread_attr);
+}
+
+/* Called with the pointer mutex held, after the cursor surface has been set. */
+static void wayland_pointer_restart_cursor_animation(void)
+{
+ struct wayland_cursor *cursor = &process_wayland.pointer.cursor;
+
+ if (cursor->num_frames <= 1) return;
+
+ pthread_once(&cursor_animation_once, cursor_animation_init);
+ if (!cursor_animation_ok) return;
+
+ clock_gettime(CLOCK_MONOTONIC, &cursor_animation_deadline);
+ timespec_add_ms(&cursor_animation_deadline,
+ cursor->frames[cursor->current_frame].delay_ms);
+ pthread_cond_signal(&cursor_animation_cond);
+}
+
static void reapply_cursor_clipping(void)
{
RECT rect;
@@ -967,6 +1170,7 @@
pointer->cursor.hotspot_x,
pointer->cursor.hotspot_y);
wayland_pointer_clear_cursor_shape();
+ wayland_pointer_restart_cursor_animation();
}
wl_display_flush(process_wayland.wl_display);
reapply_clip = TRUE;
@@ -0,0 +1,302 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] winewayland: don't take the USER lock under win_data from
client surface callbacks, post WM_CANCELMODE on keyboard leave
The client surface callbacks (update, activate, deactivate, detach, destroy,
present) run on render threads, e.g. from vkQueuePresentKHR. After the owner
selection they re-attached the client surfaces, calling NtUserGetAncestor(),
NtUserIsWindowVisible()/NtUserGetPresentRect() and, for child windows,
NtUserGetClientRect()/NtUserMapWindowPoints() with win_data_mutex held.
A thread holding the USER lock can wait for win_data through a window surface
flush (non-client paint -> window_surface_flush -> set_window_surface_contents),
so this deadlocked:
UI thread: USER lock -> window surface lock
GPU thread: window surface lock -> win_data
dxvk-submit: win_data -> USER lock
event thread: win_data (never reads Wayland events again)
Gather the toplevel, visibility and child client rect before taking win_data
and pass them to the attach path. The window-thread callers keep their
current behavior.
Also post WM_CANCELMODE from keyboard_handle_leave() instead of sending it.
It runs on the event dispatcher thread, and a synchronous send to a window
whose thread is in WM_DESTROY waiting on swapchain teardown, which itself
needs the dispatcher, hangs on exit.
https://github.com/GloriousEggroll/proton-ge-custom/issues/908
---
--- a/dlls/winewayland.drv/wayland_surface.c
+++ b/dlls/winewayland.drv/wayland_surface.c
@@ -4657,6 +4657,34 @@
static BOOL is_client_visible(HWND hwnd);
+/* Window state a client surface attach needs from win32u. Client surface
+ * callbacks run on render threads, which must not take the USER lock while
+ * holding win_data_mutex: a thread holding the USER lock can be waiting for
+ * win_data through a window surface flush. Gather it before locking. */
+struct client_attach_info
+{
+ HWND toplevel;
+ BOOL visible;
+ BOOL have_client_rect;
+ RECT client_rect;
+};
+
+static void get_client_attach_info(HWND hwnd, struct client_attach_info *info)
+{
+ info->toplevel = NtUserGetAncestor(hwnd, GA_ROOT);
+ info->visible = info->toplevel && is_client_visible(hwnd);
+ info->have_client_rect = info->toplevel && hwnd != info->toplevel;
+ if (info->have_client_rect)
+ {
+ NtUserGetClientRect(hwnd, &info->client_rect, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
+ NtUserMapWindowPoints(hwnd, info->toplevel, (POINT *)&info->client_rect, 2,
+ NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
+ }
+}
+
+static void wayland_client_surface_attach_info(struct wayland_client_surface *client, HWND toplevel,
+ const struct client_attach_info *info);
+
static void wayland_client_surface_register(struct wayland_win_data *data,
struct wayland_client_surface *surface)
{
@@ -4760,11 +4788,12 @@
wayland_client_surface_set_owner(data, replacement);
}
-void wayland_client_surfaces_attach(struct wayland_win_data *data, HWND toplevel)
+static void client_surfaces_attach_info(struct wayland_win_data *data, HWND toplevel,
+ const struct client_attach_info *info)
{
struct wayland_client_surface *surface;
struct wayland_client_surface *owner = data->client_surface;
- BOOL visible = toplevel && is_client_visible(data->hwnd);
+ BOOL visible = toplevel && (info ? info->visible : is_client_visible(data->hwnd));
/* Only native background presenters need compositor service. A pending
* native presenter must also be attached so host WSI can finish creating
@@ -4776,19 +4805,25 @@
if ((ReadAcquire(&surface->client.native_ref) ||
ReadAcquire(&surface->client.native_pending_ref)) && visible)
- wayland_client_surface_attach(surface, toplevel);
+ wayland_client_surface_attach_info(surface, toplevel, info);
else
wayland_client_surface_attach(surface, NULL);
}
if (owner)
{
- if (visible) wayland_client_surface_attach(owner, toplevel);
+ if (visible) wayland_client_surface_attach_info(owner, toplevel, info);
else wayland_client_surface_attach(owner, NULL);
}
}
-static void wayland_client_surface_update_owner(struct wayland_win_data *data)
+void wayland_client_surfaces_attach(struct wayland_win_data *data, HWND toplevel)
+{
+ client_surfaces_attach_info(data, toplevel, NULL);
+}
+
+static void wayland_client_surface_update_owner(struct wayland_win_data *data,
+ const struct client_attach_info *info)
{
struct wayland_client_surface *owner;
@@ -4798,24 +4833,26 @@
owner = data->client_surface;
if (!owner) owner = wayland_client_surface_find_owner(data, NULL, FALSE);
wayland_client_surface_set_owner(data, owner);
- wayland_client_surfaces_attach(data, NtUserGetAncestor(data->hwnd, GA_ROOT));
+ client_surfaces_attach_info(data, info->toplevel, info);
}
static void wayland_client_surface_destroy(struct client_surface *client)
{
struct wayland_client_surface *surface = impl_from_client_surface(client);
+ struct client_attach_info info;
struct wayland_win_data *data;
struct wl_callback *callback;
TRACE("%s\n", debugstr_client_surface(client));
+ get_client_attach_info(client->hwnd, &info);
if ((data = wayland_win_data_get(client->hwnd)))
{
wayland_client_surface_attach(surface, NULL);
if (data->client_surface == surface)
wayland_client_surface_restore_owner(data, surface);
wayland_client_surface_unregister(surface);
- wayland_client_surface_update_owner(data);
+ wayland_client_surface_update_owner(data, &info);
wayland_win_data_release(data);
}
else
@@ -4843,17 +4880,19 @@
static void wayland_client_surface_detach(struct client_surface *client)
{
struct wayland_client_surface *surface = impl_from_client_surface(client);
+ struct client_attach_info info;
struct wayland_win_data *data;
TRACE("%s\n", debugstr_client_surface(client));
+ get_client_attach_info(client->hwnd, &info);
if ((data = wayland_win_data_get(client->hwnd)))
{
wayland_client_surface_attach(surface, NULL);
if (data->client_surface == surface)
wayland_client_surface_restore_owner(data, surface);
wayland_client_surface_unregister(surface);
- wayland_client_surface_update_owner(data);
+ wayland_client_surface_update_owner(data, &info);
wayland_win_data_release(data);
}
InterlockedExchange(&client->presentation_owner, FALSE);
@@ -4869,15 +4908,17 @@
static void wayland_client_surface_update(struct client_surface *client)
{
struct wayland_client_surface *surface = impl_from_client_surface(client);
+ struct client_attach_info info;
struct wayland_win_data *data;
TRACE("%s\n", debugstr_client_surface(client));
+ get_client_attach_info(client->hwnd, &info);
if (!(data = wayland_win_data_get(client->hwnd))) return;
/* Preserve graphics-surface creation order while updating activity. */
wayland_client_surface_register(data, surface);
- wayland_client_surface_update_owner(data);
+ wayland_client_surface_update_owner(data, &info);
wayland_win_data_release(data);
}
@@ -4892,6 +4933,13 @@
HWND hwnd = client->client.hwnd;
RECT client_rect, dst;
+ /* Called from render threads: query win32u before taking win_data. */
+ if (hwnd != toplevel)
+ {
+ NtUserGetClientRect(hwnd, &client_rect, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
+ NtUserMapWindowPoints(hwnd, toplevel, (POINT *)&client_rect, 2, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
+ }
+
if (!(toplevel_data = wayland_win_data_get(toplevel)) ||
!(surface = toplevel_data->wayland_surface) ||
!wayland_surface_has_live_role(surface))
@@ -4907,11 +4955,6 @@
surface->window.client_rect.right - surface->window.client_rect.left,
surface->window.client_rect.bottom - surface->window.client_rect.top);
}
- else
- {
- NtUserGetClientRect(hwnd, &client_rect, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
- NtUserMapWindowPoints(hwnd, toplevel, (POINT *)&client_rect, 2, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
- }
dst = client_rect;
OffsetRect(&dst, surface->window.client_rect.left - surface->window.rect.left,
@@ -4979,11 +5022,13 @@
void set_client_surface(HWND hwnd, struct wayland_client_surface *new_client)
{
struct wayland_client_surface *owner;
+ struct client_attach_info info;
struct wayland_win_data *data;
/* ownership is shared with the callers, the last caller to release
* its reference will also destroy it and clear our pointer. */
+ get_client_attach_info(hwnd, &info);
if (!(data = wayland_win_data_get(hwnd))) return;
if (new_client)
@@ -4991,7 +5036,7 @@
owner = wayland_client_surface_find_owner(data, NULL, TRUE);
if (!owner) owner = data->client_surface ? data->client_surface : new_client;
wayland_client_surface_set_owner(data, owner);
- wayland_client_surfaces_attach(data, NtUserGetAncestor(hwnd, GA_ROOT));
+ client_surfaces_attach_info(data, info.toplevel, &info);
wayland_win_data_release(data);
}
@@ -4999,14 +5044,16 @@
{
struct wayland_client_surface *surface = impl_from_client_surface(client);
struct wayland_client_surface *owner;
+ struct client_attach_info info;
struct wayland_win_data *data;
+ get_client_attach_info(client->hwnd, &info);
if (!(data = wayland_win_data_get(client->hwnd))) return client;
wayland_client_surface_register(data, surface);
owner = wayland_client_surface_find_owner(data, NULL, TRUE);
wayland_client_surface_set_owner(data, owner);
- wayland_client_surfaces_attach(data, NtUserGetAncestor(client->hwnd, GA_ROOT));
+ client_surfaces_attach_info(data, info.toplevel, &info);
TRACE("activated %s as %s\n", debugstr_client_surface(client),
owner == surface ? "presentation owner" : "background presenter");
wayland_win_data_release(data);
@@ -5030,13 +5077,15 @@
static void wayland_client_surface_deactivate(struct client_surface *client)
{
+ struct client_attach_info info;
struct wayland_win_data *data;
+ get_client_attach_info(client->hwnd, &info);
if (!(data = wayland_win_data_get(client->hwnd))) return;
/* Keep the last frame attached across swapchain recreation. Only another
* active presenter, or actual surface detachment/destruction, replaces it. */
- wayland_client_surface_update_owner(data);
+ wayland_client_surface_update_owner(data, &info);
TRACE("deactivated %s\n", debugstr_client_surface(client));
wayland_win_data_release(data);
}
@@ -5198,6 +5247,12 @@
void wayland_client_surface_attach(struct wayland_client_surface *client, HWND toplevel)
{
+ wayland_client_surface_attach_info(client, toplevel, NULL);
+}
+
+static void wayland_client_surface_attach_info(struct wayland_client_surface *client, HWND toplevel,
+ const struct client_attach_info *info)
+{
struct wayland_win_data *toplevel_data;
struct wayland_surface *surface = NULL;
HWND hwnd = client->client.hwnd;
@@ -5291,6 +5346,10 @@
surface->window.client_rect.right - surface->window.client_rect.left,
surface->window.client_rect.bottom - surface->window.client_rect.top);
}
+ else if (info && info->have_client_rect && info->toplevel == toplevel)
+ {
+ client_rect = info->client_rect;
+ }
else
{
NtUserGetClientRect(hwnd, &client_rect, NtUserGetWinMonitorDpi(hwnd, MDT_RAW_DPI));
--- a/dlls/winewayland.drv/wayland_keyboard.c
+++ b/dlls/winewayland.drv/wayland_keyboard.c
@@ -1046,8 +1046,11 @@
wayland_cancel_layer_menu(hwnd);
else if (hwnd == foreground || wayland_is_menu_popup(hwnd))
{
+ /* Post, don't send: this runs on the event dispatcher thread, and the
+ * window's thread may be waiting for Wayland events itself, e.g. while
+ * tearing down its swapchain in WM_DESTROY. */
if (!(NtUserGetWindowLongW(hwnd, GWL_STYLE) & WS_MINIMIZE))
- send_message(hwnd, WM_CANCELMODE, 0, 0);
+ NtUserPostMessage(hwnd, WM_CANCELMODE, 0, 0);
}
if (hwnd == foreground)
@@ -0,0 +1,88 @@
From: GloriousEggroll <[email protected]>
Subject: [PATCH] winewayland: fix win_data locking in the IME text input paths
WAYLAND_SetIMECompositionRect() took win_data twice but released it once on
success, leaving the recursive win_data mutex held by the calling thread after
every IME cursor update. The event thread then blocks in text_input_enter() or
any other wayland_win_data_get(), freezing the game as soon as the IME is used.
Also use one lock order everywhere. wayland_surface_destroy() takes the
text_input mutex while holding win_data, but text_input_enter() and
WAYLAND_EnableIMEContext() took them in the reverse order. Take win_data first
in all three.
https://github.com/GloriousEggroll/proton-ge-custom/issues/798
---
--- a/dlls/winewayland.drv/wayland_text_input.c
+++ b/dlls/winewayland.drv/wayland_text_input.c
@@ -116,17 +116,17 @@
hwnd = wl_surface_get_user_data(surface);
TRACE("hwnd %p.\n", hwnd);
- pthread_mutex_lock(&text_input->mutex);
+ /* win_data_mutex before text_input->mutex, as wayland_surface_destroy()
+ * takes text_input->mutex with win_data_mutex held. */
+ if (!(data = wayland_win_data_get(hwnd))) return;
- if (!(data = wayland_win_data_get(hwnd))) goto done;
+ pthread_mutex_lock(&text_input->mutex);
text_input->focused_hwnd = hwnd;
if (data->ime_ref) wayland_text_input_enable(text_input);
- wayland_win_data_release(data);
-
-done:
pthread_mutex_unlock(&text_input->mutex);
+ wayland_win_data_release(data);
}
static void text_input_leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
@@ -262,7 +262,7 @@
TRACE("hwnd %p, rect %s.\n", hwnd, wine_dbgstr_rect(&rect));
- /* win_data_mutex before text_input->mutex, matching text_input_enter.
+ /* win_data_mutex before text_input->mutex, as in text_input_enter.
* The reverse order deadlocks against the event thread. */
if (!(data = wayland_win_data_get(hwnd)))
return FALSE;
@@ -272,14 +272,8 @@
if (!text_input->zwp_text_input_v3 || !text_input->enabled || hwnd != text_input->focused_hwnd)
goto err;
- if (!(data = wayland_win_data_get(hwnd)))
- goto err;
-
if (!(surface = data->wayland_surface) || !data->ime_ref)
- {
- wayland_win_data_release(data);
goto err;
- }
OffsetRect(&rect, -surface->window.rect.left, -surface->window.rect.top);
surface_rect = map_rect_to_surface(surface, rect);
@@ -307,10 +301,12 @@
TRACE("hwnd=%p enabled=%u\n", hwnd, enabled);
+ /* win_data_mutex before text_input->mutex, as in text_input_enter. */
+ if (!(data = wayland_win_data_get(toplevel))) return;
+
pthread_mutex_lock(&text_input->mutex);
if (!text_input->zwp_text_input_v3) goto done;
- if (!(data = wayland_win_data_get(toplevel))) goto done;
if (enabled) data->ime_ref++;
else data->ime_ref--;
@@ -320,8 +316,7 @@
if (data->ime_ref > 0) wayland_text_input_enable(text_input);
else wayland_text_input_disable(text_input);
- wayland_win_data_release(data);
-
done:
pthread_mutex_unlock(&text_input->mutex);
+ wayland_win_data_release(data);
}
@@ -0,0 +1,50 @@
From: Syrupdesu <[email protected]>
Date: Sun, 1 Nov 2026 23:10:00 -0800
Subject: [PATCH] winex11.drv: Don't wait for the pointer grab with a sent
message pending.
wait_grab_pointer() retries XGrabPointer() every 10ms until it succeeds.
That never happens when the grab is held by another thread of the same
process that is blocked in a synchronous SendMessage() to the thread
changing focus: the sender cannot release the grab before its message is
processed, and the receiver cannot process the message before the grab is
released.
Reproduced with Arknights: Endfield 1.6.3 (Steam AppID 4732690) on KWin
Wayland in desktop mode. Opening the in-game web view creates a Qt
WebEngine window and the main thread ends up in
NtUserPeekMessage -> X11DRV_ProcessEvents -> X11DRV_ClientMessage ->
set_focus -> wait_grab_pointer
while another thread of the same process is stuck in
send_inter_thread_message -> wait_message_reply. Because the pointer grab
is global, the rest of the desktop stops receiving pointer input as well,
which makes the Steam client look frozen too.
Stop waiting as soon as the thread has a sent message pending, so that
the message can be processed and the grab released. The original purpose
of the wait, delaying WM_ACTIVATE while the window manager is moving or
resizing the window, is preserved however long that takes.
---
--- a/dlls/winex11.drv/event.c
+++ b/dlls/winex11.drv/event.c
@@ -336,6 +336,18 @@
None, None, CurrentTime ) != GrabSuccess)
{
LARGE_INTEGER timeout = {.QuadPart = -10 * (ULONGLONG)10000};
+
+ /* The grab can be held by another thread of this process which is
+ * waiting for us to process a sent message, in which case it will
+ * never be released. Stop waiting so that the message can be
+ * processed. Nested driver events are not processed while we are
+ * handling an event, so checking the queue doesn't recurse here. */
+ if (HIWORD( NtUserGetQueueStatus( QS_SENDMESSAGE ) ))
+ {
+ WARN( "sent message pending, not waiting for pointer grab\n" );
+ break;
+ }
+
NtDelayExecution( FALSE, &timeout );
}
@@ -0,0 +1,49 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Tue, 20 Oct 2026 18:00:00 -0600
Subject: [PATCH] winewayland: Don't unset maximized on a window that stays
fullscreen.
Hyprland (and possibly other compositors) sends fullscreen configures with
both the fullscreen and maximized states set. A fullscreen Win32 window only
has the fullscreen state, so once such a configure is current, the next
toplevel state update sees maximized in the current state but not in the
window state and sends xdg_toplevel.unset_maximized(). Hyprland answers it
by leaving fullscreen with a maximized+tiled configure, which reaches the
application as a WM_SIZE. Wine then requests fullscreen again, and the cycle
repeats.
Since patch 0021 of this series acks fullscreen configures straight from
the requested state, the fullscreen+maximized configure becomes current
without staying in processing, so the cycle now happens on every launch.
Unreal Tournament 2004 renders into the tile-sized viewport from the
spurious WM_SIZE, and Unreal Tournament 4 drops out of fullscreen at its
splash-to-menu transition.
Fullscreen combined with maximized is the same as fullscreen (see
wayland_surface_config_is_compatible()), so leave maximized alone while
both the window and the current configure are fullscreen. Leaving
fullscreen still unsets both states.
https://github.com/GloriousEggroll/proton-ge-custom/issues/871
---
diff --git a/dlls/winewayland.drv/window.c b/dlls/winewayland.drv/window.c
--- a/dlls/winewayland.drv/window.c
+++ b/dlls/winewayland.drv/window.c
@@ -547,8 +547,15 @@
{
/* First do all state unsettings, before setting new state. Some
* Wayland compositors misbehave if the order is reversed. */
+ /* Some compositors (e.g. Hyprland) report fullscreen together with
+ * maximized. Fullscreen combined with maximized is the same as
+ * fullscreen, so keep the maximized bit while the window stays
+ * fullscreen: unsetting it makes those compositors leave fullscreen
+ * and send a tiled configure, which the application sees as a resize. */
if (!(surface->window.state & WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED) &&
- (surface->current.state & WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED))
+ (surface->current.state & WAYLAND_SURFACE_CONFIG_STATE_MAXIMIZED) &&
+ !(surface->window.state & surface->current.state &
+ WAYLAND_SURFACE_CONFIG_STATE_FULLSCREEN))
{
xdg_toplevel_unset_maximized(surface->xdg_toplevel);
}
@@ -0,0 +1,45 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: GloriousEggroll <[email protected]>
Date: Mon, 26 Oct 2026 12:00:00 -0600
Subject: [PATCH] winevulkan: Skip time domain queries for managed swapchains.
Managed cross-process swapchains (0001) are created without a host
swapchain, so their host handle is VK_NULL_HANDLE. win32u already handles
them in vkGetSwapchainTimingPropertiesEXT, vkSetSwapchainPresentTiming-
QueueSizeEXT and vkGetPastPresentationTimingEXT, but
vkGetSwapchainTimeDomainPropertiesEXT is implemented in winevulkan and
passed the null handle to the driver.
vkd3d-proton queries the time domains when the swapchain is created and
whenever its timing counters change. On NVIDIA this crashes in the driver
(__pthread_rwlock_rdlock on a null object, from libnvidia-glcore). Arc
Raiders hits it when the game window is unfocused while a level loads,
which is presumably when its swapchain takes the managed path.
Report no time domains for a swapchain without a host swapchain, like the
zeroed timing properties win32u returns for it. vkd3d-proton then does not
use present timing for that swapchain.
Bug: https://github.com/GloriousEggroll/proton-ge-custom/issues/891
Not runtime-tested.
---
diff --git a/dlls/winevulkan/vulkan.c b/dlls/winevulkan/vulkan.c
--- a/dlls/winevulkan/vulkan.c
+++ b/dlls/winevulkan/vulkan.c
@@ -768,6 +768,15 @@
TRACE("device %p, swapchain %p, properties %p, counter %p\n", device, swapchain, properties, counter);
+ /* Managed (cross-process dmabuf) swapchains have no host swapchain and no
+ * present timing, matching win32u's vkGetSwapchainTimingPropertiesEXT. */
+ if (!swapchain->host.swapchain)
+ {
+ properties->timeDomainCount = 0;
+ if (counter) *counter = 0;
+ return VK_SUCCESS;
+ }
+
host_properties.timeDomainCount = 0;
host_properties.pTimeDomainIds = NULL;
host_properties.pTimeDomains = NULL;
@@ -166,6 +166,14 @@ frame-changed `SetWindowPos`. It only acts on a live, non-minimized,
borderless window that remains in the true Wayland fullscreen state and whose
live Win32 client dimensions do not match its live window dimensions.
Patch 0086 is a second GE-Proton follow-up for 0021. Hyprland sends
fullscreen configures with both fullscreen and maximized set. Once 0021 makes
such a configure current directly, the next state update sent
`unset_maximized()` for the Win32-fullscreen window, which Hyprland answers
by leaving fullscreen with a tiled configure (GE issue 871: UT2004 viewport
and UT4 fullscreen loss). 0086 keeps maximized while both the window and the
current configure are fullscreen.
`../em-fixups/0024` is a focused backport of Erhan Bilgili's presentation
lifecycle work from `8878f044c1b0`, `88b19ffdd44b`, `a9ce7e02fe50`, and
`48bf29c49f2b`. It tracks a minimal presentation generation, invalidates it
+132 -33
View File
@@ -47,7 +47,11 @@ import protonfixes
#and edit it if needed.
import utilities
CURRENT_PREFIX_VERSION="GE-Proton11-11"
# Keep Valve's prefix version so upstream Proton and its prefix migrations
# understand prefixes we write. The GE build is logged separately (see
# proton_build_name()); older GE prefixes used "GE-ProtonX-Y" and are still
# parsed by upgrade_pfx().
CURRENT_PREFIX_VERSION="11.0-100"
PFX="Proton: "
ld_path_var = "LD_LIBRARY_PATH"
@@ -213,6 +217,32 @@ def file_exists(s, *, follow_symlinks):
def nonzero(s):
return len(s) > 0 and s != "0"
def proton_build_name():
"""The build name from this Proton's version file, e.g. "GE-Proton11-12"."""
try:
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "version"), "r") as f:
parts = f.readline().split()
return parts[1] if len(parts) > 1 else "unknown"
except OSError:
return "unknown"
def host_gpu_vendors():
"""PCI vendor IDs of the host's DRM devices, e.g. {"0x1002"} for AMD."""
vendors = set()
try:
cards = os.listdir("/sys/class/drm")
except OSError:
return vendors
for card in cards:
if not re.fullmatch(r"card[0-9]+", card):
continue
try:
with open("/sys/class/drm/" + card + "/device/vendor", "r") as f:
vendors.add(f.read().strip().lower())
except OSError:
pass
return vendors
def prepend_to_env_str(env, variable, prepend_str, separator):
if variable not in env:
env[variable] = prepend_str
@@ -842,10 +872,21 @@ class CompatData:
os.remove(self.version_file)
def upgrade_pfx(self, old_ver):
def parse_prefix_version(ver):
# Valve and most forks use "11.0-3" (or "CachyOS-11.0-100"), GE uses
# "GE-Proton11-11". Treat a GE release as major.0 with its release
# number as the prefix revision, so GE11-x vs 11.0 isn't a downgrade.
m = re.fullmatch(r'(?:.*-)?(\d+)\.(\d+)-(?:GE-)?(\d+)', ver) or \
re.fullmatch(r'GE-Proton(\d+)()-(\d+)', ver)
if not m:
raise ValueError("unrecognized prefix version " + ver)
return m.group(1), m.group(2) or "0", m.group(3)
if old_ver == CURRENT_PREFIX_VERSION:
return
log("Upgrading prefix from " + str(old_ver) + " to " + CURRENT_PREFIX_VERSION + " (" + self.base_dir + ")")
log("Upgrading prefix from " + str(old_ver) + " to " + CURRENT_PREFIX_VERSION +
" with " + proton_build_name() + " (" + self.base_dir + ")")
if old_ver is None:
return
@@ -857,10 +898,9 @@ class CompatData:
return
try:
old_proton_ver, old_prefix_ver = old_ver.split('-')
old_proton_maj, old_proton_min = old_proton_ver.split('.')
new_proton_ver, _ = CURRENT_PREFIX_VERSION.split('-')
new_proton_maj, new_proton_min = new_proton_ver.split('.')
old_proton_maj, old_proton_min, old_prefix_ver = parse_prefix_version(old_ver)
old_proton_ver = old_proton_maj + "." + old_proton_min
new_proton_maj, new_proton_min, _ = parse_prefix_version(CURRENT_PREFIX_VERSION)
if int(new_proton_maj) < int(old_proton_maj) or \
(int(new_proton_maj) == int(old_proton_maj) and \
@@ -1111,6 +1151,39 @@ class CompatData:
if tracked_name not in prev_tracked_files:
tracked_files.write(tracked_name + "\n")
self.replace_foreign_builtin_links()
def replace_foreign_builtin_links(self):
# A prefix created by another Proton install keeps symlinks to that install's builtin
# libraries wherever our default prefix has no file of the same name (e.g. Valve's
# wine-mono Microsoft.NET tree), so the walk above never refreshes them. The libraries
# are read-only, so installers that overwrite those files fail (dotnet40/48 aborts with
# 1603 after winetricks has already removed mscoree.dll). A prefix created by us has
# plain files there from wineboot, so replace the links with writable copies of our
# library of the same name, or of the old target if we don't ship one.
builtin_arch_dirs = ('i386-unix', 'i386-windows', 'x86_64-unix', 'x86_64-windows',
'aarch64-unix', 'aarch64-windows')
own_lib_dir = os.path.realpath(g_proton.lib_dir) + "/"
replaced = 0
for dir_, _, files in os.walk(self.prefix_dir + "/drive_c/windows"):
for file_ in files:
path = os.path.join(dir_, file_)
if not os.path.islink(path) or not file_is_wine_builtin_dll(path):
continue
target = os.path.normpath(os.path.join(dir_, os.readlink(path)))
if os.path.realpath(target).startswith(own_lib_dir):
continue
arch_dir = os.path.basename(os.path.dirname(target))
ours = os.path.normpath(os.path.join(g_proton.lib_dir, "wine", arch_dir, os.path.basename(target)))
os.unlink(path)
if arch_dir in builtin_arch_dirs and file_exists(ours, follow_symlinks=True):
try_copyfile(ours, path)
elif file_exists(target, follow_symlinks=True):
try_copyfile(target, path)
replaced += 1
if replaced:
log("Replaced " + str(replaced) + " builtin library links into another Proton install")
def create_symlink(self, lname, fname):
if file_exists(lname, follow_symlinks=False):
if os.path.islink(lname):
@@ -1735,32 +1808,6 @@ def default_compat_config():
]:
ret.add("disablenvapi")
if appid in [
"1808500", #ARC Raiders
"2073850", #The Finals
"108710", #Alan Wake
"202750", #Alan Wake's American Nightmare
"505170", #Carmageddon: Max Damage
"255220", #GRID Autosport
"44350", #GRID 2
"407810", #Hard Reset Redux
"233130", #Shadow Warrior
"2067160", #Simulakros
"2621010", #Simulakros Demo
"368500", #Assassin's Creed Syndicate
"1524630", #KeepUp Survival
"1233570", #Mirror's Edge Catalyst
"292030", #The Witcher 3: Wild Hunt - Remastered
]:
try:
with open('/proc/modules') as f:
drivers = set([line.partition(' ')[0] for line in f.read().splitlines()])
if not drivers.intersection({'nvidia', 'nouveau', 'nova'}):
ret.add("disablenvapi")
except OSError:
ret.add("disablenvapi")
if appid in [
"2698940", #The Crew Motorfest
"2079120", #Warudo
@@ -1917,6 +1964,23 @@ class Session:
if appid == "1018010": # Castlevania Anniversary Collection
self.env["PROTON_XINPUT_PERSISTENT_PLAYER1"] = "1"
# Rune Factory 5 shows keyboard prompts for a Steam Deck or Steam Controller.
if appid == "1702330":
self.env.setdefault("PROTON_STEAM_INPUT_XBOX_PROMPTS", "1")
# Darktide only offers its own FSR 4.1.1 on RDNA3 with its native AGS and
# without our amdxc64 FSR4 provider. Only on AMD-only systems: native AGS
# uses Wine's builtin ADL, which other vendors don't have on Windows.
# PROTON_NATIVE_AGS=0 or an amdxc64 entry in WINEDLLOVERRIDES opts out, and
# PROTON_FSR4_UPGRADE keeps amdxc64. #869
if appid == "1361210" and host_gpu_vendors() == {"0x1002"}: # Warhammer 40,000: Darktide
self.env.setdefault("PROTON_NATIVE_AGS", "1")
if (nonzero(self.env["PROTON_NATIVE_AGS"]) and
"amdxc64" not in self.env.get("WINEDLLOVERRIDES", "") and
"PROTON_FSR4_UPGRADE" not in self.env and
"fsr4" not in self.compat_config):
self.dlloverrides["amdxc64"] = "d"
# Diablo IV supports native DualSense, but does not recognize the Edge PID.
# Winebus leaves Steam Input and the physical audio/haptics identity intact.
if appid == "2344520": # Diablo IV
@@ -1925,9 +1989,12 @@ class Session:
# These games support native Sony controllers without Steam Input, but
# also assign the automatic XInput fallback to a player slot. Sackboy
# puts the DualSense on player two; Rune Factory 4 Special scrambles DS4 input.
# Where Winds Meet loses the DualSense touchpad when the controller is
# plugged in after the game starts.
if appid in [
"1599660", # Sackboy: A Big Adventure (#843)
"1580240", # Rune Factory 4 Special (#843)
"3564740", # Where Winds Meet (#829)
]:
self.env.setdefault("PROTON_SONY_AUTO_XINPUT", "0")
@@ -2001,6 +2068,16 @@ class Session:
if appid == "1850570":
self.env["PROTON_DEATH_STRANDING_FORCE_ENVIRONMENT_EFFECTS"] = "1"
# These games read the DualSense audio endpoint name and only start USB
# haptics on the Windows-style four-channel endpoint. They never probe its
# formats, so the automatic Windows Sony audio mode does not trigger. (#860)
if appid in [
"2909400", # Final Fantasy VII Rebirth
"2420110", # Horizon Forbidden West Complete Edition
"2561580", # Horizon Zero Dawn Remastered
]:
self.env.setdefault("PROTON_SONY_WINDOWS_DEVICE_NAMES", "1")
if "PROTON_PIPEWIRE_ALSA_PLUGIN" not in self.env:
if g_proton.host_pe_arch == "aarch64-windows":
alsa_arch = "aarch64-linux-gnu"
@@ -2324,6 +2401,7 @@ class Session:
self.check_environment("PROTON_DLSS_UPGRADE", "dlss")
self.check_environment("PROTON_DLSS_INDICATOR", "dlsshud")
self.check_environment("PROTON_XESS_UPGRADE", "xess")
self.check_environment("PROTON_STREAMLINE_UPGRADE", "streamline")
self.check_environment("PROTON_USE_OPTISCALER", "optiscaler")
self.check_environment("PROTON_NATIVE_AGS", "nativeags")
@@ -2331,6 +2409,7 @@ class Session:
# Preserve PROTON_USE_HDR precedence when both aliases are set.
self.check_environment("PROTON_ENABLE_HDR", "hdr")
self.check_environment("PROTON_USE_HDR", "hdr")
self.check_environment("PROTON_FORCE_HDR", "forcehdr")
self.check_environment("PROTON_NO_NTSYNC", "nontsync")
self.check_environment("PROTON_USE_WRITECOPY", "writecopy")
@@ -2578,7 +2657,12 @@ class Session:
# Leave DXVK_HDR unset by default: shared DXGI detects HDR for DX11/DX12
# from Wine Wayland output state or Gamescope XWayland feedback.
# Retain legacy opt-ins/opt-outs without changing the display driver.
if "hdr" in self.compat_config:
# forcehdr reports HDR on Wine Wayland outputs the compositor reports
# as SDR (KWin at SDR brightness == peak, Hyprland cm_auto_hdr).
if "forcehdr" in self.compat_config:
self.env["DXVK_HDR"] = "1"
self.env["WAYLANDDRV_FORCE_HDR"] = "1"
elif "hdr" in self.compat_config:
self.env["DXVK_HDR"] = "1"
elif "PROTON_ENABLE_HDR" in self.env or "PROTON_USE_HDR" in self.env:
self.env["DXVK_HDR"] = "0"
@@ -2769,6 +2853,20 @@ class Session:
else:
self.remote_debug_cmd = None
if ("disablenvapi" not in self.compat_config) and ("forcenvapi" not in self.compat_config):
try:
with open('/proc/modules') as f:
drivers = set([line.partition(' ')[0] for line in f.read().splitlines()])
if not drivers.intersection({'nvidia', 'nouveau', 'nova'}):
if self.log_file is not None:
self.log_file.write("No NV driver found, disabling nvapi\n")
self.compat_config.add("disablenvapi")
except:
if self.log_file is not None:
self.log_file.write("Error: unable to check /proc/modules for NV driver presence\n")
if self.log_file is not None:
self.log_file.flush()
if update_prefix_files:
g_compatdata.setup_prefix()
g_compatdata.sync_appcache()
@@ -2945,6 +3043,7 @@ if __name__ == "__main__":
g_session.check_environment("PROTON_MLFG_UPGRADE", "mlfg")
g_session.check_environment("PROTON_DLSS_UPGRADE", "dlss")
g_session.check_environment("PROTON_XESS_UPGRADE", "xess")
g_session.check_environment("PROTON_STREAMLINE_UPGRADE", "streamline")
g_session.check_environment("PROTON_USE_OPTISCALER", "optiscaler")
protonfixes.setup_upscalers(g_session.compat_config, g_session.env, g_compatdata.base_dir, g_compatdata.prefix_dir)
protonfixes.execute()
+1 -1
Submodule vkd3d updated: fed0c3418f...23254b531c.
+41
View File
@@ -1911,6 +1911,29 @@ XrResult WINAPI xrEnumerateSpatialAnchorAttachableComponentsANDROID(XrInstance i
return params.result;
}
XrResult WINAPI xrEnumerateSpatialArchetypeComponentTypesEXT(XrSpatialSnapshotEXT snapshot, XrSpatialArchetypeIdEXT archetype, uint32_t componentTypeCapacityInput, uint32_t *componentTypeCountOutput, XrSpatialComponentTypeEXT *componentTypes)
{
struct xrEnumerateSpatialArchetypeComponentTypesEXT_params params;
params.snapshot = snapshot;
params.archetype = archetype;
params.componentTypeCapacityInput = componentTypeCapacityInput;
params.componentTypeCountOutput = componentTypeCountOutput;
params.componentTypes = componentTypes;
UNIX_CALL_CHECKED(xrEnumerateSpatialArchetypeComponentTypesEXT, &params);
return params.result;
}
XrResult WINAPI xrEnumerateSpatialArchetypesEXT(XrSpatialSnapshotEXT snapshot, uint32_t archetypeCapacityInput, uint32_t *archetypeCountOutput, XrSpatialArchetypeIdEXT *archetypes)
{
struct xrEnumerateSpatialArchetypesEXT_params params;
params.snapshot = snapshot;
params.archetypeCapacityInput = archetypeCapacityInput;
params.archetypeCountOutput = archetypeCountOutput;
params.archetypes = archetypes;
UNIX_CALL_CHECKED(xrEnumerateSpatialArchetypesEXT, &params);
return params.result;
}
XrResult WINAPI xrEnumerateSpatialCapabilitiesEXT(XrInstance instance, XrSystemId systemId, uint32_t capabilityCapacityInput, uint32_t *capabilityCountOutput, XrSpatialCapabilityEXT *capabilities)
{
struct xrEnumerateSpatialCapabilitiesEXT_params params;
@@ -2210,6 +2233,15 @@ XrResult WINAPI xrGetAnchorUuidBD(XrAnchorBD anchor, XrUuidEXT *uuid)
return params.result;
}
XrResult WINAPI xrGetAppConsumedSwapchainPropertiesEXT(XrAppConsumedSwapchainEXT appConsumedSwapchain, XrAppConsumedSwapchainPropertiesEXT *properties)
{
struct xrGetAppConsumedSwapchainPropertiesEXT_params params;
params.appConsumedSwapchain = appConsumedSwapchain;
params.properties = properties;
UNIX_CALL_CHECKED(xrGetAppConsumedSwapchainPropertiesEXT, &params);
return params.result;
}
XrResult WINAPI xrGetAudioInputDeviceGuidOculus(XrInstance instance, wchar_t buffer[XR_MAX_AUDIO_DEVICE_STR_SIZE_OCULUS])
{
struct xrGetAudioInputDeviceGuidOculus_params params;
@@ -4630,6 +4662,7 @@ XrResult WINAPI xrWaitSwapchainImage(XrSwapchain swapchain, const XrSwapchainIma
static const struct openxr_func xr_instance_dispatch_table[] =
{
{"xrAcquireAppConsumedSwapchainImageEXT", xrAcquireAppConsumedSwapchainImageEXT},
{"xrAcquireSwapchainImage", xrAcquireSwapchainImage},
{"xrAddTrackableImageDatabaseANDROID", xrAddTrackableImageDatabaseANDROID},
{"xrAllocateWorldMeshBufferML", xrAllocateWorldMeshBufferML},
@@ -4656,6 +4689,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrCreateAnchorSpaceANDROID", xrCreateAnchorSpaceANDROID},
{"xrCreateAnchorSpaceBD", xrCreateAnchorSpaceBD},
{"xrCreateApiLayerInstance", xrCreateApiLayerInstance},
{"xrCreateAppConsumedSwapchainEXT", xrCreateAppConsumedSwapchainEXT},
{"xrCreateBodyTrackerBD", xrCreateBodyTrackerBD},
{"xrCreateBodyTrackerFB", xrCreateBodyTrackerFB},
{"xrCreateBodyTrackerHTC", xrCreateBodyTrackerHTC},
@@ -4750,6 +4784,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrDestroyAction", xrDestroyAction},
{"xrDestroyActionSet", xrDestroyActionSet},
{"xrDestroyAnchorBD", xrDestroyAnchorBD},
{"xrDestroyAppConsumedSwapchainEXT", xrDestroyAppConsumedSwapchainEXT},
{"xrDestroyBodyTrackerBD", xrDestroyBodyTrackerBD},
{"xrDestroyBodyTrackerFB", xrDestroyBodyTrackerFB},
{"xrDestroyBodyTrackerHTC", xrDestroyBodyTrackerHTC},
@@ -4816,6 +4851,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrEndSession", xrEndSession},
{"xrEndSpatialContainerRenderingEXT", xrEndSpatialContainerRenderingEXT},
{"xrEnumerateApiLayerProperties", xrEnumerateApiLayerProperties},
{"xrEnumerateAppConsumedSwapchainImagesEXT", xrEnumerateAppConsumedSwapchainImagesEXT},
{"xrEnumerateBoundSourcesForAction", xrEnumerateBoundSourcesForAction},
{"xrEnumerateColorSpacesFB", xrEnumerateColorSpacesFB},
{"xrEnumerateColorSpacesSONY", xrEnumerateColorSpacesSONY},
@@ -4840,6 +4876,8 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrEnumerateSceneComputeFeaturesMSFT", xrEnumerateSceneComputeFeaturesMSFT},
{"xrEnumerateSpaceSupportedComponentsFB", xrEnumerateSpaceSupportedComponentsFB},
{"xrEnumerateSpatialAnchorAttachableComponentsANDROID", xrEnumerateSpatialAnchorAttachableComponentsANDROID},
{"xrEnumerateSpatialArchetypeComponentTypesEXT", xrEnumerateSpatialArchetypeComponentTypesEXT},
{"xrEnumerateSpatialArchetypesEXT", xrEnumerateSpatialArchetypesEXT},
{"xrEnumerateSpatialCapabilitiesEXT", xrEnumerateSpatialCapabilitiesEXT},
{"xrEnumerateSpatialCapabilityComponentTypesEXT", xrEnumerateSpatialCapabilityComponentTypesEXT},
{"xrEnumerateSpatialCapabilityFeaturesEXT", xrEnumerateSpatialCapabilityFeaturesEXT},
@@ -4853,6 +4891,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrEnumerateSupportedSpatialContainerGraphicsPresentationsEXT", xrEnumerateSupportedSpatialContainerGraphicsPresentationsEXT},
{"xrEnumerateSupportedTrackableTypesANDROID", xrEnumerateSupportedTrackableTypesANDROID},
{"xrEnumerateSwapchainFormats", xrEnumerateSwapchainFormats},
{"xrEnumerateSwapchainFormats2EXT", xrEnumerateSwapchainFormats2EXT},
{"xrEnumerateSwapchainImages", xrEnumerateSwapchainImages},
{"xrEnumerateViewConfigurationViews", xrEnumerateViewConfigurationViews},
{"xrEnumerateViewConfigurations", xrEnumerateViewConfigurations},
@@ -4869,6 +4908,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrGetAllTrackablesANDROID", xrGetAllTrackablesANDROID},
{"xrGetAnchorPersistStateANDROID", xrGetAnchorPersistStateANDROID},
{"xrGetAnchorUuidBD", xrGetAnchorUuidBD},
{"xrGetAppConsumedSwapchainPropertiesEXT", xrGetAppConsumedSwapchainPropertiesEXT},
{"xrGetAudioInputDeviceGuidOculus", xrGetAudioInputDeviceGuidOculus},
{"xrGetAudioOutputDeviceGuidOculus", xrGetAudioOutputDeviceGuidOculus},
{"xrGetBodySkeletonFB", xrGetBodySkeletonFB},
@@ -5029,6 +5069,7 @@ static const struct openxr_func xr_instance_dispatch_table[] =
{"xrQuerySpatialComponentDataEXT", xrQuerySpatialComponentDataEXT},
{"xrQuerySystemTrackedKeyboardFB", xrQuerySystemTrackedKeyboardFB},
{"xrRaycastANDROID", xrRaycastANDROID},
{"xrReleaseAppConsumedSwapchainImageEXT", xrReleaseAppConsumedSwapchainImageEXT},
{"xrReleaseSwapchainImage", xrReleaseSwapchainImage},
{"xrRemoveTrackableImageDatabaseANDROID", xrRemoveTrackableImageDatabaseANDROID},
{"xrRequestBodyTrackingFidelityMETA", xrRequestBodyTrackingFidelityMETA},
+83
View File
@@ -26,6 +26,7 @@ enum unix_call
{
unix_init,
unix_is_available_instance_function,
unix_xrAcquireAppConsumedSwapchainImageEXT,
unix_xrAcquireSwapchainImage,
unix_xrAddTrackableImageDatabaseANDROID,
unix_xrAllocateWorldMeshBufferML,
@@ -51,6 +52,7 @@ enum unix_call
unix_xrCreateActionSpace,
unix_xrCreateAnchorSpaceANDROID,
unix_xrCreateAnchorSpaceBD,
unix_xrCreateAppConsumedSwapchainEXT,
unix_xrCreateBodyTrackerBD,
unix_xrCreateBodyTrackerFB,
unix_xrCreateBodyTrackerHTC,
@@ -143,6 +145,7 @@ enum unix_call
unix_xrDestroyAction,
unix_xrDestroyActionSet,
unix_xrDestroyAnchorBD,
unix_xrDestroyAppConsumedSwapchainEXT,
unix_xrDestroyBodyTrackerBD,
unix_xrDestroyBodyTrackerFB,
unix_xrDestroyBodyTrackerHTC,
@@ -209,6 +212,7 @@ enum unix_call
unix_xrEndSession,
unix_xrEndSpatialContainerRenderingEXT,
unix_xrEnumerateApiLayerProperties,
unix_xrEnumerateAppConsumedSwapchainImagesEXT,
unix_xrEnumerateBoundSourcesForAction,
unix_xrEnumerateColorSpacesFB,
unix_xrEnumerateColorSpacesSONY,
@@ -233,6 +237,8 @@ enum unix_call
unix_xrEnumerateSceneComputeFeaturesMSFT,
unix_xrEnumerateSpaceSupportedComponentsFB,
unix_xrEnumerateSpatialAnchorAttachableComponentsANDROID,
unix_xrEnumerateSpatialArchetypeComponentTypesEXT,
unix_xrEnumerateSpatialArchetypesEXT,
unix_xrEnumerateSpatialCapabilitiesEXT,
unix_xrEnumerateSpatialCapabilityComponentTypesEXT,
unix_xrEnumerateSpatialCapabilityFeaturesEXT,
@@ -246,6 +252,7 @@ enum unix_call
unix_xrEnumerateSupportedSpatialContainerGraphicsPresentationsEXT,
unix_xrEnumerateSupportedTrackableTypesANDROID,
unix_xrEnumerateSwapchainFormats,
unix_xrEnumerateSwapchainFormats2EXT,
unix_xrEnumerateSwapchainImages,
unix_xrEnumerateViewConfigurationViews,
unix_xrEnumerateViewConfigurations,
@@ -262,6 +269,7 @@ enum unix_call
unix_xrGetAllTrackablesANDROID,
unix_xrGetAnchorPersistStateANDROID,
unix_xrGetAnchorUuidBD,
unix_xrGetAppConsumedSwapchainPropertiesEXT,
unix_xrGetAudioInputDeviceGuidOculus,
unix_xrGetAudioOutputDeviceGuidOculus,
unix_xrGetBodySkeletonFB,
@@ -418,6 +426,7 @@ enum unix_call
unix_xrQuerySpatialComponentDataEXT,
unix_xrQuerySystemTrackedKeyboardFB,
unix_xrRaycastANDROID,
unix_xrReleaseAppConsumedSwapchainImageEXT,
unix_xrReleaseSwapchainImage,
unix_xrRemoveTrackableImageDatabaseANDROID,
unix_xrRequestBodyTrackingFidelityMETA,
@@ -512,6 +521,14 @@ enum unix_call
unix_count,
};
struct xrAcquireAppConsumedSwapchainImageEXT_params
{
XrAppConsumedSwapchainEXT DECLSPEC_ALIGN(8) appConsumedSwapchain;
const XrAppConsumedSwapchainImageAcquireInfoEXT *acquireInfo;
XrAppConsumedSwapchainImageStateEXT *image;
XrResult result;
};
struct xrAcquireSwapchainImage_params
{
XrSwapchain DECLSPEC_ALIGN(8) swapchain;
@@ -701,6 +718,14 @@ struct xrCreateAnchorSpaceBD_params
XrResult result;
};
struct xrCreateAppConsumedSwapchainEXT_params
{
XrSession DECLSPEC_ALIGN(8) session;
const XrAppConsumedSwapchainCreateInfoEXT *createInfo;
XrAppConsumedSwapchainEXT *appConsumedSwapchain;
XrResult result;
};
struct xrCreateBodyTrackerBD_params
{
XrSession DECLSPEC_ALIGN(8) session;
@@ -1434,6 +1459,12 @@ struct xrDestroyAnchorBD_params
XrResult result;
};
struct xrDestroyAppConsumedSwapchainEXT_params
{
XrAppConsumedSwapchainEXT DECLSPEC_ALIGN(8) appConsumedSwapchain;
XrResult result;
};
struct xrDestroyBodyTrackerBD_params
{
XrBodyTrackerBD DECLSPEC_ALIGN(8) bodyTracker;
@@ -1840,6 +1871,15 @@ struct xrEnumerateApiLayerProperties_params
XrResult result;
};
struct xrEnumerateAppConsumedSwapchainImagesEXT_params
{
XrAppConsumedSwapchainEXT DECLSPEC_ALIGN(8) appConsumedSwapchain;
uint32_t imageCapacityInput;
uint32_t *imageCountOutput;
XrSwapchainImageBaseHeader *images;
XrResult result;
};
struct xrEnumerateBoundSourcesForAction_params
{
XrSession DECLSPEC_ALIGN(8) session;
@@ -2067,6 +2107,25 @@ struct xrEnumerateSpatialAnchorAttachableComponentsANDROID_params
XrResult result;
};
struct xrEnumerateSpatialArchetypeComponentTypesEXT_params
{
XrSpatialSnapshotEXT DECLSPEC_ALIGN(8) snapshot;
XrSpatialArchetypeIdEXT DECLSPEC_ALIGN(8) archetype;
uint32_t componentTypeCapacityInput;
uint32_t *componentTypeCountOutput;
XrSpatialComponentTypeEXT *componentTypes;
XrResult result;
};
struct xrEnumerateSpatialArchetypesEXT_params
{
XrSpatialSnapshotEXT DECLSPEC_ALIGN(8) snapshot;
uint32_t archetypeCapacityInput;
uint32_t *archetypeCountOutput;
XrSpatialArchetypeIdEXT *archetypes;
XrResult result;
};
struct xrEnumerateSpatialCapabilitiesEXT_params
{
XrInstance DECLSPEC_ALIGN(8) instance;
@@ -2196,6 +2255,16 @@ struct xrEnumerateSwapchainFormats_params
XrResult result;
};
struct xrEnumerateSwapchainFormats2EXT_params
{
XrSession DECLSPEC_ALIGN(8) session;
XrSwapchainFormatsEnumerateInfoEXT *enumerateInfo;
uint32_t formatCapacityInput;
uint32_t *formatCountOutput;
int64_t *formats;
XrResult result;
};
struct xrEnumerateSwapchainImages_params
{
XrSwapchain DECLSPEC_ALIGN(8) swapchain;
@@ -2330,6 +2399,13 @@ struct xrGetAnchorUuidBD_params
XrResult result;
};
struct xrGetAppConsumedSwapchainPropertiesEXT_params
{
XrAppConsumedSwapchainEXT DECLSPEC_ALIGN(8) appConsumedSwapchain;
XrAppConsumedSwapchainPropertiesEXT *properties;
XrResult result;
};
struct xrGetAudioInputDeviceGuidOculus_params
{
XrInstance DECLSPEC_ALIGN(8) instance;
@@ -3592,6 +3668,13 @@ struct xrRaycastANDROID_params
XrResult result;
};
struct xrReleaseAppConsumedSwapchainImageEXT_params
{
XrAppConsumedSwapchainEXT DECLSPEC_ALIGN(8) appConsumedSwapchain;
const XrAppConsumedSwapchainImageReleaseInfoEXT *releaseInfo;
XrResult result;
};
struct xrReleaseSwapchainImage_params
{
XrSwapchain DECLSPEC_ALIGN(8) swapchain;
+11 -2
View File
@@ -65,7 +65,7 @@ from enum import Enum
LOGGER = logging.Logger("openxr")
LOGGER.addHandler(logging.StreamHandler())
XR_XML_VERSION = "1.1.63"
XR_XML_VERSION = "1.1.65"
# Filenames to create.
WINE_OPENXR_H = "wineopenxr.h"
@@ -149,6 +149,7 @@ MANUAL_UNIX_THUNKS = {
"xrCreateSession",
"xrDestroySession",
"xrCreateSwapchain",
"xrCreateAppConsumedSwapchainEXT",
"xrGetInstanceProcAddr",
"xrEnumerateInstanceExtensionProperties",
"xrConvertTimeToWin32PerformanceCounterKHR",
@@ -180,11 +181,17 @@ MANUAL_LOADER_THUNKS = {
"xrDestroySession",
"xrGetSystem",
"xrEnumerateSwapchainFormats",
"xrEnumerateSwapchainFormats2EXT",
"xrCreateSwapchain",
"xrCreateAppConsumedSwapchainEXT",
"xrDestroySwapchain",
"xrDestroyAppConsumedSwapchainEXT",
"xrEnumerateSwapchainImages",
"xrEnumerateAppConsumedSwapchainImagesEXT",
"xrAcquireSwapchainImage",
"xrAcquireAppConsumedSwapchainImageEXT",
"xrReleaseSwapchainImage",
"xrReleaseAppConsumedSwapchainImageEXT",
"xrBeginFrame",
"xrEndFrame",
"xrGetVulkanDeviceExtensionsKHR",
@@ -918,6 +925,8 @@ class Handle(Type):
return "wine_session_from_handle({0})->host_session".format(name)
if self.name == "XrSwapchain":
return "wine_swapchain_from_handle({0})->host_swapchain".format(name)
if self.name == "XrAppConsumedSwapchainEXT":
return "((XrAppConsumedSwapchainEXT)wine_swapchain_from_app_consumed_swapchain_handle({0})->host_swapchain)".format(name)
if self.is_dispatchable():
LOGGER.error("Unhandled host handle for: {0}".format(self.name))
@@ -1445,7 +1454,7 @@ class Parameter(VkVariable):
self.format_conv = "wine_dbgstr_longlong({0})"
elif self.type_name in ["uint16_t", "uint32_t", "XrBool32"]:
self.format_str = "%u"
elif self.type_name in ["uint64_t", "XrAsyncRequestIdFB", "XrSystemId", "XrPath", "XrTime", "XrDuration", "XrControllerModelKeyMSFT", "XrMarkerML", "XrSpatialEntityIdBD", "XrCameraImageIdBD"]:
elif self.type_name in ["uint64_t", "XrAsyncRequestIdFB", "XrSystemId", "XrPath", "XrTime", "XrDuration", "XrControllerModelKeyMSFT", "XrMarkerML", "XrSpatialEntityIdBD", "XrCameraImageIdBD", "XrSpatialArchetypeIdEXT"]:
self.format_str = "0x%s"
self.format_conv = "wine_dbgstr_longlong({0})"
elif self.type_name in ["HANDLE", "VkInstance"]:
+26 -19
View File
@@ -221,6 +221,13 @@ XrResult WINAPI wine_xrCreateSwapchain(XrSession session,
createInfo, swapchain);
}
XrResult WINAPI wine_xrCreateAppConsumedSwapchainEXT(XrSession session,
const XrAppConsumedSwapchainCreateInfoEXT *createInfo,
XrAppConsumedSwapchainEXT *swapchain) {
return g_xr_host_instance_dispatch_table.p_xrCreateAppConsumedSwapchainEXT(wine_session_from_handle(session)->host_session,
createInfo, swapchain);
}
XrResult WINAPI wine_xrEnumerateInstanceExtensionProperties(const char *layerName,
uint32_t propertyCapacityInput,
uint32_t *propertyCountOutput,
@@ -341,7 +348,8 @@ XrResult WINAPI wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance,
static const char win32_surface[] = "VK_KHR_surface VK_KHR_win32_surface";
XrResult res;
uint32_t lin_len;
char internal_exts[4096];
uint32_t size;
TRACE("%p, 0x%s, %u, %p, %p\n", (void *)(uintptr_t)instance, wine_dbgstr_longlong(systemId), bufferCapacityInput, bufferCountOutput,
buffer);
@@ -351,27 +359,26 @@ XrResult WINAPI wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance,
* add it here. */
res = g_xr_host_instance_dispatch_table.p_xrGetVulkanInstanceExtensionsKHR(
wine_instance_from_handle(instance)->host_instance, systemId, bufferCapacityInput, bufferCountOutput, buffer);
if (res == XR_SUCCESS) {
if (bufferCapacityInput > 0) {
/* *bufferCountOutput is not required to (and sometimes does not) contain the offset to the NUL byte */
lin_len = strlen(buffer) + 1;
wine_instance_from_handle(instance)->host_instance, systemId, sizeof(internal_exts),
&size, internal_exts);
if (bufferCapacityInput < lin_len + sizeof(win32_surface)) {
return XR_ERROR_SIZE_INSUFFICIENT;
}
buffer[lin_len - 1] = ' ';
memcpy(&buffer[lin_len], win32_surface, sizeof(win32_surface));
TRACE("returning: %s\n", buffer);
*bufferCountOutput = lin_len + sizeof(win32_surface);
} else {
*bufferCountOutput += sizeof(win32_surface) /* NUL byte included for required ' ' */;
}
if (res)
{
ERR("params.result %d, size %u.\n", res, size);
return res;
}
return res;
size = strlen(internal_exts) + 1;
internal_exts[size - 1] = ' ';
memcpy(&internal_exts[size], win32_surface, sizeof(win32_surface));
size = strlen(internal_exts) + 1;
*bufferCountOutput = size;
if (bufferCapacityInput < size)
return bufferCapacityInput ? XR_ERROR_SIZE_INSUFFICIENT : XR_SUCCESS;
memcpy(buffer, internal_exts, size);
TRACE("returning: %s\n", buffer);
return XR_SUCCESS;
}
static VkResult WINAPI vk_create_instance_callback(const VkInstanceCreateInfo *create_info,
+339 -83
View File
@@ -161,11 +161,13 @@ done:
RegCloseKey(vr_key);
return FALSE;
}
size = sizeof(g_physdev_vid);
if ((status = RegQueryValueExA(vr_key, "openxr_vulkan_device_vid", NULL, &type, (BYTE *)&g_physdev_vid, &size))) {
ERR("Error getting openxr_vulkan_device_vid, status: %#lx.\n", status);
RegCloseKey(vr_key);
return FALSE;
}
size = sizeof(g_physdev_pid);
if ((status = RegQueryValueExA(vr_key, "openxr_vulkan_device_pid", NULL, &type, (BYTE *)&g_physdev_pid, &size))) {
ERR("Error getting openxr_vulkan_device_pid, status: %#lx.\n", status);
RegCloseKey(vr_key);
@@ -706,6 +708,7 @@ static BOOL is_vulkan_format_depth(VkFormat format) {
return FALSE;
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return TRUE;
@@ -801,6 +804,7 @@ int64_t map_format_vulkan_to_dxgi(int64_t format) {
case VK_FORMAT_D16_UNORM:
return DXGI_FORMAT_D16_UNORM;
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D24_UNORM_S8_UINT:
return DXGI_FORMAT_D24_UNORM_S8_UINT;
@@ -813,6 +817,33 @@ int64_t map_format_vulkan_to_dxgi(int64_t format) {
}
}
static XrResult map_vulkan_formats_to_dxgi(int64_t *real_formats, uint32_t real_format_count, int64_t *formats,
uint32_t formatCapacityInput, uint32_t *formatCountOutput)
{
uint32_t i, o;
XrResult ret = XR_SUCCESS;
o = 0;
for (i = 0; i < real_format_count; ++i) {
int64_t mapped = map_format_vulkan_to_dxgi(real_formats[i]);
if (mapped == DXGI_FORMAT_UNKNOWN) {
continue;
}
if (formatCapacityInput && formats) {
if (o < formatCapacityInput) {
formats[o] = mapped;
} else {
ret = XR_ERROR_SIZE_INSUFFICIENT;
}
}
++o;
}
*formatCountOutput = o;
return ret;
}
XrResult WINAPI xrEnumerateSwapchainFormats(XrSession session,
uint32_t formatCapacityInput,
uint32_t *formatCountOutput,
@@ -820,7 +851,6 @@ XrResult WINAPI xrEnumerateSwapchainFormats(XrSession session,
wine_XrSession *wine_session = wine_session_from_handle(session);
uint32_t real_format_count;
int64_t *real_formats;
uint32_t i, o;
struct xrEnumerateSwapchainFormats_params params = {
.session = session,
.formatCapacityInput = formatCapacityInput,
@@ -854,24 +884,47 @@ XrResult WINAPI xrEnumerateSwapchainFormats(XrSession session,
goto done;
}
o = 0;
for (i = 0; i < real_format_count; ++i) {
int64_t mapped = map_format_vulkan_to_dxgi(real_formats[i]);
params.result = map_vulkan_formats_to_dxgi(real_formats, real_format_count, formats, formatCapacityInput, formatCountOutput);
if (mapped == DXGI_FORMAT_UNKNOWN) {
continue;
}
done:
free(real_formats);
return params.result;
}
if (formatCapacityInput && formats) {
if (o < formatCapacityInput) {
formats[o] = mapped;
} else {
params.result = XR_ERROR_SIZE_INSUFFICIENT;
}
}
++o;
XrResult WINAPI xrEnumerateSwapchainFormats2EXT(XrSession session, XrSwapchainFormatsEnumerateInfoEXT *enumerateInfo, uint32_t formatCapacityInput, uint32_t *formatCountOutput, int64_t *formats)
{
wine_XrSession *wine_session = wine_session_from_handle(session);
uint32_t real_format_count;
int64_t *real_formats;
struct xrEnumerateSwapchainFormats2EXT_params params = {
.session = session, .enumerateInfo = enumerateInfo, .formatCapacityInput = formatCapacityInput, .formatCountOutput = formatCountOutput,
.formats = formats,
};
if (wine_session->session_type != SESSION_TYPE_D3D11 && wine_session->session_type != SESSION_TYPE_D3D12) {
UNIX_CALL_CHECKED(xrEnumerateSwapchainFormats2EXT, &params);
return params.result;
}
*formatCountOutput = o;
params.formatCapacityInput = 0;
params.formatCountOutput = &real_format_count;
params.formats = NULL;
UNIX_CALL_CHECKED(xrEnumerateSwapchainFormats2EXT, &params);
if (params.result != XR_SUCCESS) {
return params.result;
}
real_formats = malloc(sizeof(*real_formats) * real_format_count);
params.formatCapacityInput = real_format_count;
params.formats = real_formats;
UNIX_CALL_CHECKED(xrEnumerateSwapchainFormats2EXT, &params);
if (params.result != XR_SUCCESS)
goto done;
params.result = map_vulkan_formats_to_dxgi(real_formats, real_format_count, formats, formatCapacityInput, formatCountOutput);
done:
free(real_formats);
@@ -885,8 +938,6 @@ XrResult WINAPI xrCreateSwapchain(XrSession session, const XrSwapchainCreateInfo
struct xrCreateSwapchain_params params = {
.session = session, .createInfo = &our_createInfo, .swapchain = &wine_swapchain->host_swapchain};
wine_swapchain->create_info = *createInfo;
TRACE("%#I64x, %p, %p\n", (ULONG64)session, createInfo, swapchain);
if (wine_session->session_type == SESSION_TYPE_D3D11 || wine_session->session_type == SESSION_TYPE_D3D12) {
@@ -916,12 +967,86 @@ XrResult WINAPI xrCreateSwapchain(XrSession session, const XrSwapchainCreateInfo
return params.result;
}
wine_swapchain->usage_flags = createInfo->usageFlags;
wine_swapchain->format = createInfo->format;
wine_swapchain->width = createInfo->width;
wine_swapchain->height = createInfo->height;
wine_swapchain->array_size = createInfo->arraySize;
wine_swapchain->mip_count = createInfo->mipCount;
wine_swapchain->sample_count = createInfo->sampleCount;
wine_swapchain->session = wine_session;
*swapchain = (XrSwapchain)(ULONG_PTR)wine_swapchain;
return XR_SUCCESS;
}
XrResult WINAPI xrCreateAppConsumedSwapchainEXT(XrSession session, const XrAppConsumedSwapchainCreateInfoEXT *createInfo, XrAppConsumedSwapchainEXT *swapchain) {
wine_XrSession *wine_session = wine_session_from_handle(session);
wine_XrSwapchain *wine_swapchain = calloc(1, sizeof(wine_XrSwapchain));
XrAppConsumedSwapchainCreateInfoEXT our_createInfo = *createInfo;
struct xrCreateAppConsumedSwapchainEXT_params params = {
.session = session, .createInfo = &our_createInfo, .appConsumedSwapchain = (XrAppConsumedSwapchainEXT *)&wine_swapchain->host_swapchain};
XrAppConsumedSwapchainPropertiesEXT props;
struct xrGetAppConsumedSwapchainPropertiesEXT_params prop_params = { .properties = &props };
TRACE("%#I64x, %p, %p\n", (ULONG64)session, createInfo, swapchain);
if (wine_session->session_type == SESSION_TYPE_D3D11 || wine_session->session_type == SESSION_TYPE_D3D12) {
BOOL format_is_depth;
our_createInfo.format = map_format_dxgi_to_vulkan(createInfo->format);
if (our_createInfo.format == VK_FORMAT_UNDEFINED) {
ERR("unable to set Vulkan format\n");
free(wine_swapchain);
return XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED;
}
our_createInfo.usageFlags |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT;
format_is_depth = is_vulkan_format_depth(our_createInfo.format);
if ((createInfo->usageFlags & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT) && format_is_depth) {
WARN("Swapchain has a color attachment usage, but it's format is for depth\n");
}
if ((createInfo->usageFlags & XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) && !format_is_depth) {
WARN("Swapchain has a depth/stencil usage, but it's format is for color\n");
}
}
UNIX_CALL_CHECKED(xrCreateAppConsumedSwapchainEXT, &params);
if (params.result != XR_SUCCESS) {
WARN("xrCreateSwapchain failed: %d\n", params.result);
free(wine_swapchain);
return params.result;
}
wine_swapchain->session = wine_session;
prop_params.appConsumedSwapchain = (XrAppConsumedSwapchainEXT)(ULONG_PTR)wine_swapchain;
UNIX_CALL_CHECKED(xrGetAppConsumedSwapchainPropertiesEXT, &prop_params);
if (params.result != XR_SUCCESS) {
struct xrDestroyAppConsumedSwapchainEXT_params params = { .appConsumedSwapchain = (XrAppConsumedSwapchainEXT)(ULONG_PTR)wine_swapchain };
ERR("xrGetAppConsumedSwapchainPropertiesEXT failed.\n");
UNIX_CALL_CHECKED(xrDestroyAppConsumedSwapchainEXT, &params);
return params.result;
}
wine_swapchain->app_consumed = TRUE;
wine_swapchain->usage_flags = props.usageFlags;
wine_swapchain->format = props.format;
if (wine_session->session_type == SESSION_TYPE_D3D11 || wine_session->session_type == SESSION_TYPE_D3D12)
wine_swapchain->format = map_format_vulkan_to_dxgi(wine_swapchain->format);
wine_swapchain->width = props.width;
wine_swapchain->height = props.height;
wine_swapchain->array_size = props.arraySize;
wine_swapchain->mip_count = props.mipCount;
wine_swapchain->sample_count = props.sampleCount;
*swapchain = (XrAppConsumedSwapchainEXT)(ULONG_PTR)wine_swapchain;
return XR_SUCCESS;
}
static void release_d3d12_resources(wine_XrSwapchain *swapchain, uint32_t image_count) {
wine_XrInstance *instance = swapchain->session->instance;
XrSwapchainImageD3D12KHR *d3d12_images = (XrSwapchainImageD3D12KHR *)swapchain->images;
@@ -943,18 +1068,8 @@ static void release_d3d12_resources(wine_XrSwapchain *swapchain, uint32_t image_
free(swapchain->acquired_indices);
}
XrResult WINAPI xrDestroySwapchain(XrSwapchain swapchain) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle(swapchain);
struct xrDestroySwapchain_params params = {.swapchain = swapchain};
TRACE("%#I64x.\n", (ULONG64)swapchain);
UNIX_CALL_CHECKED(xrDestroySwapchain, &params);
if (params.result != XR_SUCCESS) {
WARN("xrDestroySwapchain failed: %d\n", params.result);
return params.result;
}
static void destroy_wine_swapchain(wine_XrSwapchain *wine_swapchain)
{
if (wine_swapchain->image_count) {
UINT i;
if (wine_swapchain->session->session_type == SESSION_TYPE_D3D11) {
@@ -970,7 +1085,37 @@ XrResult WINAPI xrDestroySwapchain(XrSwapchain swapchain) {
}
free(wine_swapchain);
}
XrResult WINAPI xrDestroySwapchain(XrSwapchain swapchain) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle(swapchain);
struct xrDestroySwapchain_params params = {.swapchain = swapchain};
TRACE("%#I64x.\n", (ULONG64)swapchain);
UNIX_CALL_CHECKED(xrDestroySwapchain, &params);
if (params.result != XR_SUCCESS) {
WARN("xrDestroySwapchain failed: %d\n", params.result);
return params.result;
}
destroy_wine_swapchain(wine_swapchain);
return XR_SUCCESS;
}
XrResult WINAPI xrDestroyAppConsumedSwapchainEXT(XrAppConsumedSwapchainEXT swapchain) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle((XrSwapchain)swapchain);
struct xrDestroyAppConsumedSwapchainEXT_params params = {.appConsumedSwapchain = swapchain};
TRACE("%#I64x.\n", (ULONG64)swapchain);
UNIX_CALL_CHECKED(xrDestroyAppConsumedSwapchainEXT, &params);
if (params.result != XR_SUCCESS) {
WARN("xrDestroyAppConsumedSwapchainEXT failed: %d\n", params.result);
return params.result;
}
destroy_wine_swapchain(wine_swapchain);
return XR_SUCCESS;
}
@@ -1045,38 +1190,60 @@ static VkResult record_transition_command(wine_XrInstance *instance,
return vkEndCommandBuffer(*out_cmd);
}
XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
static XrResult WINAPI enumerate_swapchain_images(wine_XrSwapchain *wine_swapchain,
uint32_t imageCapacityInput,
uint32_t *imageCountOutput,
XrSwapchainImageBaseHeader *images) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle(swapchain);
wine_XrInstance *wine_instance = wine_swapchain->session->instance;
XrSwapchainImageVulkanKHR *our_vk;
HRESULT hr;
size_t image_size = 0;
uint32_t i;
struct xrEnumerateSwapchainImages_params params = {
.swapchain = swapchain,
.swapchain = (XrSwapchain)(uintptr_t)wine_swapchain,
.imageCapacityInput = imageCapacityInput,
.imageCountOutput = imageCountOutput,
.images = images,
};
struct xrEnumerateAppConsumedSwapchainImagesEXT_params ac_params = {
.appConsumedSwapchain = (XrAppConsumedSwapchainEXT)(uintptr_t)wine_swapchain,
.imageCapacityInput = imageCapacityInput,
.imageCountOutput = imageCountOutput,
.images = images,
};
TRACE("%#I64x, %u, %p, %p\n", (ULONG64)swapchain, imageCapacityInput, imageCountOutput, images);
TRACE("%p, %u, %p, %p\n", wine_swapchain, imageCapacityInput, imageCountOutput, images);
if (wine_swapchain->session->session_type != SESSION_TYPE_D3D11 &&
wine_swapchain->session->session_type != SESSION_TYPE_D3D12) {
if (wine_swapchain->app_consumed)
{
UNIX_CALL_CHECKED(xrEnumerateAppConsumedSwapchainImagesEXT, &ac_params);
return params.result;
}
UNIX_CALL_CHECKED(xrEnumerateSwapchainImages, &params);
return params.result;
}
if (!wine_swapchain->image_count) {
uint32_t image_count;
params.imageCapacityInput = 0;
params.imageCountOutput = &image_count;
params.images = NULL;
UNIX_CALL_CHECKED(xrEnumerateSwapchainImages, &params);
if (params.result != XR_SUCCESS) {
return params.result;
if (wine_swapchain->app_consumed)
{
ac_params.imageCapacityInput = 0;
ac_params.imageCountOutput = &image_count;
ac_params.images = NULL;
UNIX_CALL_CHECKED(xrEnumerateAppConsumedSwapchainImagesEXT, &ac_params);
if (ac_params.result != XR_SUCCESS)
return ac_params.result;
}
else
{
params.imageCapacityInput = 0;
params.imageCountOutput = &image_count;
params.images = NULL;
UNIX_CALL_CHECKED(xrEnumerateSwapchainImages, &params);
if (params.result != XR_SUCCESS)
return params.result;
}
our_vk = malloc(sizeof(*our_vk) * image_count);
@@ -1085,28 +1252,41 @@ XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
our_vk[i].next = NULL;
}
params.imageCapacityInput = image_count;
params.images = (XrSwapchainImageBaseHeader *)our_vk;
UNIX_CALL_CHECKED(xrEnumerateSwapchainImages, &params);
if (params.result != XR_SUCCESS) {
free(our_vk);
return params.result;
if (wine_swapchain->app_consumed)
{
ac_params.imageCapacityInput = image_count;
ac_params.images = (XrSwapchainImageBaseHeader *)our_vk;
UNIX_CALL_CHECKED(xrEnumerateAppConsumedSwapchainImagesEXT, &ac_params);
if (ac_params.result != XR_SUCCESS) {
free(our_vk);
return ac_params.result;
}
}
else
{
params.imageCapacityInput = image_count;
params.images = (XrSwapchainImageBaseHeader *)our_vk;
UNIX_CALL_CHECKED(xrEnumerateSwapchainImages, &params);
if (params.result != XR_SUCCESS) {
free(our_vk);
return params.result;
}
}
if (wine_swapchain->session->session_type == SESSION_TYPE_D3D11) {
XrSwapchainImageD3D11KHR *our_d3d11;
D3D11_TEXTURE2D_DESC1 desc;
desc.Width = wine_swapchain->create_info.width;
desc.Height = wine_swapchain->create_info.height;
desc.MipLevels = wine_swapchain->create_info.mipCount;
desc.ArraySize = wine_swapchain->create_info.arraySize;
desc.Format = wine_swapchain->create_info.format;
desc.Width = wine_swapchain->width;
desc.Height = wine_swapchain->height;
desc.MipLevels = wine_swapchain->mip_count;
desc.ArraySize = wine_swapchain->array_size;
desc.Format = wine_swapchain->format;
TRACE("creating dxvk texture with dxgi format %d (%x)\n", desc.Format, desc.Format);
desc.SampleDesc.Count = wine_swapchain->create_info.sampleCount;
desc.SampleDesc.Count = wine_swapchain->sample_count;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = d3d11usage_from_XrSwapchainUsageFlags(wine_swapchain->create_info.usageFlags);
desc.BindFlags = d3d11usage_from_XrSwapchainUsageFlags(wine_swapchain->usage_flags);
desc.CPUAccessFlags = 0;
desc.MiscFlags = 0;
desc.TextureLayout = D3D11_TEXTURE_LAYOUT_UNDEFINED;
@@ -1134,16 +1314,16 @@ XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
ID3D12DeviceExt1 *device_ext;
HRESULT hr = wine_instance->d3d12_device->lpVtbl->QueryInterface(wine_instance->d3d12_device,
&IID_ID3D12DeviceExt1, (void **)&device_ext);
BOOL format_is_depth = is_vulkan_format_depth(map_format_dxgi_to_vulkan(wine_swapchain->create_info.format));
BOOL format_is_depth = is_vulkan_format_depth(map_format_dxgi_to_vulkan(wine_swapchain->format));
BOOL succeeded = TRUE;
const D3D12_RESOURCE_STATES incoming_state =
format_is_depth ? D3D12_RESOURCE_STATE_DEPTH_WRITE : D3D12_RESOURCE_STATE_RENDER_TARGET;
const VkImageLayout vk_layout =
format_is_depth ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
const VkImageSubresourceRange subresource = {
.layerCount = wine_swapchain->create_info.arraySize,
.layerCount = wine_swapchain->array_size,
.baseArrayLayer = 0,
.levelCount = wine_swapchain->create_info.mipCount,
.levelCount = wine_swapchain->mip_count,
.baseMipLevel = 0,
.aspectMask =
format_is_depth ? (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) : VK_IMAGE_ASPECT_COLOR_BIT,
@@ -1155,27 +1335,33 @@ XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
desc.Alignment = 0;
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = wine_swapchain->create_info.width;
desc.Height = wine_swapchain->create_info.height;
desc.MipLevels = wine_swapchain->create_info.mipCount;
desc.DepthOrArraySize = wine_swapchain->create_info.arraySize;
desc.Format = wine_swapchain->create_info.format;
desc.Width = wine_swapchain->width;
desc.Height = wine_swapchain->height;
desc.MipLevels = wine_swapchain->mip_count;
desc.DepthOrArraySize = wine_swapchain->array_size;
desc.Format = wine_swapchain->format;
TRACE("creating vkd3d-proton texture with dxgi format %d (%x)\n", desc.Format, desc.Format);
desc.SampleDesc.Count = wine_swapchain->create_info.sampleCount;
desc.SampleDesc.Count = wine_swapchain->sample_count;
desc.SampleDesc.Quality = 0;
desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
desc.Flags = 0;
if (!format_is_depth) {
if (wine_swapchain->usage_flags & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT)
{
if (format_is_depth)
ERR("XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT with depth format.\n");
desc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
} else {
}
if (wine_swapchain->usage_flags & XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)
{
if (!format_is_depth)
ERR("XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT with color format.\n");
desc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
if (!(wine_swapchain->create_info.usageFlags & XR_SWAPCHAIN_USAGE_SAMPLED_BIT)) {
desc.Flags |= D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE;
}
}
if (wine_swapchain->create_info.usageFlags & XR_SWAPCHAIN_USAGE_UNORDERED_ACCESS_BIT) {
if (!(wine_swapchain->usage_flags & XR_SWAPCHAIN_USAGE_SAMPLED_BIT))
desc.Flags |= D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE;
if (wine_swapchain->usage_flags & XR_SWAPCHAIN_USAGE_UNORDERED_ACCESS_BIT)
desc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
our_d3d12 = calloc(1, sizeof(XrSwapchainImageD3D12KHR) * image_count);
for (i = 0; i < image_count; ++i) {
@@ -1235,6 +1421,36 @@ XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
return XR_SUCCESS;
}
XrResult WINAPI xrEnumerateSwapchainImages(XrSwapchain swapchain,
uint32_t imageCapacityInput,
uint32_t *imageCountOutput,
XrSwapchainImageBaseHeader *images) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle(swapchain);
if (wine_swapchain->app_consumed)
{
ERR( "app consumed swapchain.\n" );
return XR_ERROR_HANDLE_INVALID;
}
return enumerate_swapchain_images(wine_swapchain, imageCapacityInput, imageCountOutput, images);
}
XrResult WINAPI xrEnumerateAppConsumedSwapchainImagesEXT(XrAppConsumedSwapchainEXT swapchain,
uint32_t imageCapacityInput,
uint32_t *imageCountOutput,
XrSwapchainImageBaseHeader *images) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle((XrSwapchain)swapchain);
if (!wine_swapchain->app_consumed)
{
ERR( "not app consumed swapchain.\n" );
return XR_ERROR_HANDLE_INVALID;
}
return enumerate_swapchain_images(wine_swapchain, imageCapacityInput, imageCountOutput, images);
}
static void lock_d3d_queue(wine_XrInstance *instance, BOOL drain_queue)
{
if (instance->dxvk_device)
@@ -1312,6 +1528,26 @@ XrResult WINAPI xrAcquireSwapchainImage(XrSwapchain swapchain,
return params.result;
}
XrResult WINAPI xrAcquireAppConsumedSwapchainImageEXT(XrAppConsumedSwapchainEXT appConsumedSwapchain,
const XrAppConsumedSwapchainImageAcquireInfoEXT *acquireInfo,
XrAppConsumedSwapchainImageStateEXT *image)
{
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle((XrSwapchain)appConsumedSwapchain);
wine_XrInstance *wine_instance = wine_swapchain->session->instance;
struct xrAcquireAppConsumedSwapchainImageEXT_params params =
{
.appConsumedSwapchain = appConsumedSwapchain,
.acquireInfo = acquireInfo,
.image = image,
};
lock_d3d_queue(wine_instance, FALSE);
UNIX_CALL_CHECKED(xrAcquireAppConsumedSwapchainImageEXT, &params);
unlock_d3d_queue(wine_instance, FALSE);
return params.result;
}
XrResult WINAPI xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchainImageReleaseInfo *releaseInfo) {
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle(swapchain);
wine_XrInstance *wine_instance = wine_swapchain->session->instance;
@@ -1369,6 +1605,23 @@ XrResult WINAPI xrReleaseSwapchainImage(XrSwapchain swapchain, const XrSwapchain
return params.result;
}
XrResult WINAPI xrReleaseAppConsumedSwapchainImageEXT(XrAppConsumedSwapchainEXT appConsumedSwapchain,
const XrAppConsumedSwapchainImageReleaseInfoEXT *releaseInfo)
{
wine_XrSwapchain *wine_swapchain = wine_swapchain_from_handle((XrSwapchain)appConsumedSwapchain);
wine_XrInstance *wine_instance = wine_swapchain->session->instance;
struct xrReleaseAppConsumedSwapchainImageEXT_params params =
{
.appConsumedSwapchain = appConsumedSwapchain,
.releaseInfo = releaseInfo,
};
lock_d3d_queue(wine_instance, TRUE);
UNIX_CALL_CHECKED(xrReleaseAppConsumedSwapchainImageEXT, &params);
unlock_d3d_queue(wine_instance, TRUE);
return params.result;
}
XrResult WINAPI xrBeginFrame(XrSession session, const XrFrameBeginInfo *frameBeginInfo) {
wine_XrSession *wine_session = wine_session_from_handle(session);
struct xrBeginFrame_params params = {
@@ -1630,30 +1883,33 @@ XrResult WINAPI xrNegotiateLoaderRuntimeInterface(const XrNegotiateLoaderInfo *i
XrResult WINAPI xrGetVulkanDeviceExtensionsKHR(XrInstance instance, XrSystemId systemId, uint32_t bufferCapacityInput, uint32_t *bufferCountOutput, char *buffer)
{
struct xrGetVulkanDeviceExtensionsKHR_params params;
char internal_exts[4096];
uint32_t size;
/* Even while returning fixed string still call the host function, that is a part of OpenXR over Vulkan
* expected initialization sequence. */
params.instance = instance;
params.systemId = systemId;
params.bufferCapacityInput = bufferCapacityInput;
params.bufferCountOutput = bufferCountOutput;
params.buffer = buffer;
params.bufferCapacityInput = bufferCapacityInput ? sizeof(internal_exts) : 0;
params.bufferCountOutput = &size;
params.buffer = bufferCapacityInput ? internal_exts : NULL;
UNIX_CALL_CHECKED(xrGetVulkanDeviceExtensionsKHR, &params);
if (params.result == XR_SUCCESS && bufferCapacityInput)
if (params.result)
{
__wine_set_unix_env(WINE_VULKAN_DEVICE_VARIABLE, buffer);
strcpy(buffer, WINE_VULKAN_DEVICE_EXTENSION_NAME);
*bufferCountOutput = sizeof(WINE_VULKAN_DEVICE_EXTENSION_NAME);
TRACE("returning %s.\n", buffer);
}
else if ((params.result == XR_SUCCESS || params.result == XR_ERROR_SIZE_INSUFFICIENT)
&& *bufferCountOutput < sizeof(WINE_VULKAN_DEVICE_EXTENSION_NAME))
{
*bufferCountOutput = sizeof(WINE_VULKAN_DEVICE_EXTENSION_NAME);
ERR("params.result %d, size %u.\n", params.result, size);
return params.result;
}
return params.result;
*bufferCountOutput = sizeof(WINE_VULKAN_DEVICE_EXTENSION_NAME);
if (bufferCapacityInput < *bufferCountOutput)
return bufferCapacityInput ? XR_ERROR_SIZE_INSUFFICIENT : XR_SUCCESS;
__wine_set_unix_env(WINE_VULKAN_DEVICE_VARIABLE, internal_exts);
strcpy(buffer, WINE_VULKAN_DEVICE_EXTENSION_NAME);
TRACE("returning %s.\n", buffer);
return XR_SUCCESS;
}
+9 -1
View File
@@ -94,7 +94,11 @@ typedef struct {
BOOL *acquired;
uint32_t *acquired_indices;
XrSwapchainCreateInfo create_info;
BOOL app_consumed;
XrSwapchainUsageFlags usage_flags;
int64_t format;
uint32_t width, height, array_size, mip_count, sample_count;
VkCommandBuffer *cmd_release;
VkCommandBuffer *cmd_acquire;
} wine_XrSwapchain;
@@ -103,6 +107,10 @@ static inline wine_XrSwapchain *wine_swapchain_from_handle(XrSwapchain handle) {
return (wine_XrSwapchain *)(uintptr_t)handle;
}
static inline wine_XrSwapchain *wine_swapchain_from_app_consumed_swapchain_handle(XrAppConsumedSwapchainEXT handle) {
return (wine_XrSwapchain *)(uintptr_t)handle;
}
void *wine_xr_get_instance_proc_addr(const char *name);
struct openxr_func {
+1074 -222
View File
File diff suppressed because it is too large. Load diff
+10
View File
@@ -27,6 +27,7 @@
/* Functions for which we have custom implementations outside of the thunks. */
XrResult wine_xrConvertTimeToWin32PerformanceCounterKHR(XrInstance instance, XrTime time, LARGE_INTEGER *performanceCounter);
XrResult wine_xrConvertWin32PerformanceCounterToTimeKHR(XrInstance instance, const LARGE_INTEGER *performanceCounter, XrTime *time);
XrResult wine_xrCreateAppConsumedSwapchainEXT(XrSession session, const XrAppConsumedSwapchainCreateInfoEXT *createInfo, XrAppConsumedSwapchainEXT *appConsumedSwapchain);
XrResult wine_xrCreateInstance(const XrInstanceCreateInfo *createInfo, XrInstance *instance);
XrResult wine_xrCreateSession(XrInstance instance, const XrSessionCreateInfo *createInfo, XrSession *session);
XrResult wine_xrCreateSwapchain(XrSession session, const XrSwapchainCreateInfo *createInfo, XrSwapchain *swapchain);
@@ -37,6 +38,7 @@ XrResult wine_xrGetVulkanGraphicsDeviceKHR(XrInstance instance, XrSystemId syste
XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId systemId, uint32_t bufferCapacityInput, uint32_t *bufferCountOutput, char *buffer);
#define ALL_XR_INSTANCE_FUNCS() \
USE_XR_FUNC(xrAcquireAppConsumedSwapchainImageEXT) \
USE_XR_FUNC(xrAcquireSwapchainImage) \
USE_XR_FUNC(xrAddTrackableImageDatabaseANDROID) \
USE_XR_FUNC(xrAllocateWorldMeshBufferML) \
@@ -63,6 +65,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrCreateAnchorSpaceANDROID) \
USE_XR_FUNC(xrCreateAnchorSpaceBD) \
USE_XR_FUNC(xrCreateApiLayerInstance) \
USE_XR_FUNC(xrCreateAppConsumedSwapchainEXT) \
USE_XR_FUNC(xrCreateBodyTrackerBD) \
USE_XR_FUNC(xrCreateBodyTrackerFB) \
USE_XR_FUNC(xrCreateBodyTrackerHTC) \
@@ -156,6 +159,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrDestroyAction) \
USE_XR_FUNC(xrDestroyActionSet) \
USE_XR_FUNC(xrDestroyAnchorBD) \
USE_XR_FUNC(xrDestroyAppConsumedSwapchainEXT) \
USE_XR_FUNC(xrDestroyBodyTrackerBD) \
USE_XR_FUNC(xrDestroyBodyTrackerFB) \
USE_XR_FUNC(xrDestroyBodyTrackerHTC) \
@@ -222,6 +226,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrEndSession) \
USE_XR_FUNC(xrEndSpatialContainerRenderingEXT) \
USE_XR_FUNC(xrEnumerateApiLayerProperties) \
USE_XR_FUNC(xrEnumerateAppConsumedSwapchainImagesEXT) \
USE_XR_FUNC(xrEnumerateBoundSourcesForAction) \
USE_XR_FUNC(xrEnumerateColorSpacesFB) \
USE_XR_FUNC(xrEnumerateColorSpacesSONY) \
@@ -246,6 +251,8 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrEnumerateSceneComputeFeaturesMSFT) \
USE_XR_FUNC(xrEnumerateSpaceSupportedComponentsFB) \
USE_XR_FUNC(xrEnumerateSpatialAnchorAttachableComponentsANDROID) \
USE_XR_FUNC(xrEnumerateSpatialArchetypeComponentTypesEXT) \
USE_XR_FUNC(xrEnumerateSpatialArchetypesEXT) \
USE_XR_FUNC(xrEnumerateSpatialCapabilitiesEXT) \
USE_XR_FUNC(xrEnumerateSpatialCapabilityComponentTypesEXT) \
USE_XR_FUNC(xrEnumerateSpatialCapabilityFeaturesEXT) \
@@ -259,6 +266,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrEnumerateSupportedSpatialContainerGraphicsPresentationsEXT) \
USE_XR_FUNC(xrEnumerateSupportedTrackableTypesANDROID) \
USE_XR_FUNC(xrEnumerateSwapchainFormats) \
USE_XR_FUNC(xrEnumerateSwapchainFormats2EXT) \
USE_XR_FUNC(xrEnumerateSwapchainImages) \
USE_XR_FUNC(xrEnumerateViewConfigurationViews) \
USE_XR_FUNC(xrEnumerateViewConfigurations) \
@@ -275,6 +283,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrGetAllTrackablesANDROID) \
USE_XR_FUNC(xrGetAnchorPersistStateANDROID) \
USE_XR_FUNC(xrGetAnchorUuidBD) \
USE_XR_FUNC(xrGetAppConsumedSwapchainPropertiesEXT) \
USE_XR_FUNC(xrGetAudioInputDeviceGuidOculus) \
USE_XR_FUNC(xrGetAudioOutputDeviceGuidOculus) \
USE_XR_FUNC(xrGetBodySkeletonFB) \
@@ -434,6 +443,7 @@ XrResult wine_xrGetVulkanInstanceExtensionsKHR(XrInstance instance, XrSystemId s
USE_XR_FUNC(xrQuerySpatialComponentDataEXT) \
USE_XR_FUNC(xrQuerySystemTrackedKeyboardFB) \
USE_XR_FUNC(xrRaycastANDROID) \
USE_XR_FUNC(xrReleaseAppConsumedSwapchainImageEXT) \
USE_XR_FUNC(xrReleaseSwapchainImage) \
USE_XR_FUNC(xrRemoveTrackableImageDatabaseANDROID) \
USE_XR_FUNC(xrRequestBodyTrackingFidelityMETA) \
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more