mirror of
https://github.com/Droid-Deck/DroidDeck-Proton.git
synced 2026-11-07 02:36:05 -10:00
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 for98efe1cdande15b1e15are 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:
commit
ef7f031388
102 files changed
+8094
-2363
No files matched your search
@@ -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? -->
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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
Submodule d7vk updated: bcd66d0c95...06656b9e94.
+1
-1
Submodule dxvk updated: 2cb40186c4...f863776806.
+1
-1
Submodule dxvk-nvapi updated: 0f26995d81...9c0f214cb6.
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
@@ -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, ¶ms );
|
||||
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
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, ¶ms );
|
||||
@@ -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, ¶ms );
|
||||
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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, ¤t)
|
||||
+ && (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);
|
||||
|
||||
+53
@@ -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)
|
||||
{
|
||||
+32
@@ -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;
|
||||
|
||||
+57
@@ -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");
|
||||
+64
@@ -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)
|
||||
{
|
||||
+138
@@ -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;
|
||||
+101
@@ -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
|
||||
+114
@@ -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;
|
||||
+66
@@ -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, ¶ms );
|
||||
@@ -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, ¶ms );
|
||||
+402
@@ -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, ¶ms );
|
||||
+ 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, ¶ms );
|
||||
+ 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( ¶ms._ret, inputHandle, TRUE )) return params._ret;
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput001_GetInputTypeForHandle, ¶ms );
|
||||
+ params._ret = steaminput_native_input_type( params._ret );
|
||||
return params._ret;
|
||||
}
|
||||
|
||||
@@ -755,6 +790,23 @@
|
||||
};
|
||||
TRACE("%p\n", _this);
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput002_GetDigitalActionOrigins, ¶ms );
|
||||
+ 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, ¶ms );
|
||||
+ 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( ¶ms._ret, inputHandle, FALSE )) return params._ret;
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput002_GetInputTypeForHandle, ¶ms );
|
||||
+ params._ret = steaminput_native_input_type( params._ret );
|
||||
return params._ret;
|
||||
}
|
||||
|
||||
@@ -1364,6 +1434,23 @@
|
||||
};
|
||||
TRACE("%p\n", _this);
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput005_GetDigitalActionOrigins, ¶ms );
|
||||
+ 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, ¶ms );
|
||||
+ 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( ¶ms._ret, inputHandle, FALSE )) return params._ret;
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput005_GetInputTypeForHandle, ¶ms );
|
||||
+ params._ret = steaminput_native_input_type( params._ret );
|
||||
return params._ret;
|
||||
}
|
||||
|
||||
@@ -2055,6 +2160,23 @@
|
||||
};
|
||||
TRACE("%p\n", _this);
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput006_GetDigitalActionOrigins, ¶ms );
|
||||
+ 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, ¶ms );
|
||||
+ 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( ¶ms._ret, inputHandle, FALSE )) return params._ret;
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput006_GetInputTypeForHandle, ¶ms );
|
||||
+ params._ret = steaminput_native_input_type( params._ret );
|
||||
return params._ret;
|
||||
}
|
||||
|
||||
@@ -2759,6 +2899,23 @@
|
||||
};
|
||||
TRACE("%p\n", _this);
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput007_GetDigitalActionOrigins, ¶ms );
|
||||
+ 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, ¶ms );
|
||||
+ 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( ¶ms._ret, inputHandle, FALSE )) return params._ret;
|
||||
STEAMCLIENT_CALL( ISteamInput_SteamInput007_GetInputTypeForHandle, ¶ms );
|
||||
+ 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
|
||||
}
|
||||
+63
-42
@@ -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, ¶ms);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+61
-33
@@ -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
|
||||
|
||||
+11
-7
@@ -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 @@
|
||||
|
||||
+10
-10
@@ -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
|
||||
@@ -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
+45
@@ -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
|
||||
|
||||
+105
@@ -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 ))
|
||||
+52
@@ -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')
|
||||
+1
-1
@@ -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) \
|
||||
+121
@@ -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, ¶ms[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();
|
||||
+62
@@ -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) );
|
||||
+110
@@ -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 );
|
||||
+49
-35
@@ -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();
|
||||
+73
@@ -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
|
||||
+122
@@ -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 )
|
||||
+204
@@ -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 );
|
||||
+101
@@ -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 );
|
||||
+174
@@ -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;
|
||||
+302
@@ -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)
|
||||
+88
@@ -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 );
|
||||
}
|
||||
|
||||
+49
@@ -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);
|
||||
}
|
||||
+45
@@ -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
|
||||
|
||||
@@ -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 protonfixes updated: f4940924ee...28a02c25e4.
+1
-1
Submodule vkd3d updated: fed0c3418f...23254b531c.
+1
-1
Submodule vkd3d-proton updated: 0bb64c56c8...8293130ebe.
@@ -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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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, ¶ms);
|
||||
return params.result;
|
||||
}
|
||||
*formatCountOutput = o;
|
||||
|
||||
params.formatCapacityInput = 0;
|
||||
params.formatCountOutput = &real_format_count;
|
||||
params.formats = NULL;
|
||||
|
||||
UNIX_CALL_CHECKED(xrEnumerateSwapchainFormats2EXT, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
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, ¶ms);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
Reference in new issue
Block a user