From c54454dc5aea69835acd9a00b63d9bfbb9cce73a Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Mon, 10 Aug 2026 14:21:31 +0900 Subject: [PATCH 001/110] batocera-system: bring forward A/B-common Kconfig gating fixes and KERNEL_SRCDIR dev mount from odroid-m1-panfrost These were found while bringing up the Panfrost (B) image but are generic bugfixes unrelated to which GPU stack the board uses: - rocknix-joypad/sdl3-x11 exclusion on this board was keyed off the blob package name (!BR2_PACKAGE_MALI_G52_ODROIDM1) as a stand-in for "this is the odroid-m1 board" - swapped to the always-present board identifier (BR2_PACKAGE_UBOOT_ODROID_M1) so it stays correct regardless of GPU stack. - TRX's desktop-GL requirement was similarly keyed off the blob package name instead of the real capability flag (BR2_PACKAGE_HAS_LIBGL). - moonlight-qt's guard is left untouched - its exclusion reason (a blob-specific eglGetPlatformDisplay gap) is genuinely GPU-driver-specific. - docker.mk's KERNEL_SRCDIR opt-in bind mount (buildroot's OVERRIDE_SRCDIR mechanism, see board/batocera/rockchip/bsp-odroidm1/local.mk.example on odroid-m1-panfrost) is harmless to have available on this branch too. No functional change for this branch's build output (blob still gates HAS_LIBGL/board-identity the same way as before), this is purely a root-cause correctness fix so the two branches don't drift on unrelated-to-GPU logic. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- docker/docker.mk | 16 ++++++++++++++++ package/batocera/core/batocera-system/Config.in | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docker/docker.mk b/docker/docker.mk index fa44d61c299..afb73e3baa2 100644 --- a/docker/docker.mk +++ b/docker/docker.mk @@ -23,6 +23,22 @@ else UID := $(shell id -u) GID := $(shell id -g) +# Opt-in extra bind mount for iterative package development: point +# KERNEL_SRCDIR (or any _SRCDIR= via EXTRA_SRCDIR_MOUNTS, see +# below) at a local git checkout and it shows up in the container at +# /kernel-src, ready for a _OVERRIDE_SRCDIR=/kernel-src in local.mk +# (see output//local.mk - BR2_PACKAGE_OVERRIDE_FILE's default +# location). This is buildroot's own documented mechanism for "edit +# source, rebuild, no commit/push needed" - see buildroot manual's "Using +# Buildroot during development". Without this mount, only a version +# string that buildroot has ALREADY cached under is enough to make it +# silently keep reusing a stale download() forever, even after new +# commits land upstream - this bit the odroid-m1-panfrost kernel branch +# for two full iterations (see Joplin "buildroot 커널 캐시 오염" note). +ifdef KERNEL_SRCDIR +DOCKER_OPTS += -v $(KERNEL_SRCDIR):/kernel-src +endif + define RUN_DOCKER $(DOCKER) run -t --init --rm \ -v $(PROJECT_DIR):/build \ diff --git a/package/batocera/core/batocera-system/Config.in b/package/batocera/core/batocera-system/Config.in index f155f0ce28e..9d94c8ca360 100644 --- a/package/batocera/core/batocera-system/Config.in +++ b/package/batocera/core/batocera-system/Config.in @@ -348,7 +348,7 @@ config BR2_PACKAGE_BATOCERA_SYSTEM select BR2_PACKAGE_ROCKNIX_JOYPAD if (BR2_PACKAGE_BATOCERA_TARGET_RK3326 || \ BR2_PACKAGE_BATOCERA_TARGET_RK3568 || \ BR2_PACKAGE_BATOCERA_TARGET_H700) && \ - !BR2_PACKAGE_MALI_G52_ODROIDM1 + !BR2_PACKAGE_UBOOT_ODROID_M1 select BR2_PACKAGE_UPOWER # required by some cemuhook servers @@ -1225,7 +1225,7 @@ config BR2_PACKAGE_BATOCERA_PORTS_SYSTEMS select BR2_PACKAGE_LIBRETRO_BOOM3 if BR2_PACKAGE_BATOCERA_TARGET_X86_64_ANY # Requires RA with OpenGL # Tomb Raider 1, 2 & 3 - select BR2_PACKAGE_TRX if BR2_PACKAGE_XORG7 && !BR2_PACKAGE_MALI_G52_ODROIDM1 + select BR2_PACKAGE_TRX if BR2_PACKAGE_XORG7 && BR2_PACKAGE_HAS_LIBGL # Quake 3 select BR2_PACKAGE_IOQUAKE3 if !BR2_PACKAGE_BATOCERA_TARGET_BCM2835 && \ @@ -2260,7 +2260,7 @@ config BR2_PACKAGE_BATOCERA_SDL3 select BR2_PACKAGE_SDL3_TTF select BR2_PACKAGE_SDL3_OPENGLES if BR2_PACKAGE_HAS_LIBGLES select BR2_PACKAGE_SDL3_OPENGL if BR2_PACKAGE_HAS_LIBGL - select BR2_PACKAGE_SDL3_X11 if BR2_PACKAGE_XORG7 && !BR2_PACKAGE_MALI_G52_ODROIDM1 + select BR2_PACKAGE_SDL3_X11 if BR2_PACKAGE_XORG7 && !BR2_PACKAGE_UBOOT_ODROID_M1 select BR2_PACKAGE_SDL3_KMSDRM if BR2_PACKAGE_HAS_LIBGBM && !BR2_PACKAGE_BATOCERA_RPI_VCORE select BR2_PACKAGE_SDL3_WAYLAND if BR2_PACKAGE_BATOCERA_WAYLAND select BR2_PACKAGE_BATOCERA_HAS_SDL From 397b00abf74ec67c44968d9c0efc3c1c31bc732b Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Mon, 10 Aug 2026 16:38:44 +0900 Subject: [PATCH 002/110] buildroot: bump submodule for mesa3d-headers version fix (26.1.4 -> 26.1.5, matches mesa3d.mk/hash) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- buildroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildroot b/buildroot index a7564de8bd1..95d26b3e3ff 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit a7564de8bd1b9e2a477d02abf7c73414d422b848 +Subproject commit 95d26b3e3ff0a88446aaed4ab2a27c9504ae838c From da6b3b6fd23b3ee816754d4954d676b49b62b58c Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 15:33:56 +0900 Subject: [PATCH 003/110] odroidm1: cap BR2_JLEVEL=8 to stop 17-way parallel g++ from OOM-killing the build host Confirmed via journalctl: the Claude Code process itself got OOM-killed mid-build (systemd: "ptyxis-spawn-...scope: A process of this unit has been killed by the OOM killer") while a from-scratch build was compiling MAME/OpenCV4/Kodi at BR2_JLEVEL=0's default (nproc+1=17 on this 16-core/ 15GB host) - wide parallelism multiplies peak per-translation-unit memory for large C++ files, not just wall-clock speed. odroid-m1-panfrost hit the same class of problem with LLVM+Mesa3D and already caps at 8; this board never got the equivalent cap despite also building large C++ codebases (this was the first from-scratch build of this board's output tree in a while, so the 17-way default had never actually been exercised under this kind of memory pressure before). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index e739f2e1baa..34444abe796 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -47,6 +47,18 @@ BR2_PACKAGE_HOST_DTC=y BR2_PACKAGE_BATOCERA_KODI21=y BR2_TARGET_ROOTFS_SQUASHFS4_ZSTD=y +# Host-machine parallelism cap - BR2_JLEVEL=0 (auto = nproc+1 = 17 on the +# LG gram build host, 16-core/15GB) OOM-killed the Claude Code process +# itself (confirmed via journalctl: "ptyxis-spawn-...scope: A process of +# this unit has been killed by the OOM killer") while 17-way parallel g++ +# was compiling MAME/OpenCV4/Kodi - large individual translation units, +# not just many small ones, so wide parallelism multiplies peak per-TU +# memory instead of just wall-clock speed. odroid-m1-panfrost hit the same +# class of problem with LLVM+Mesa3D and capped at 4 (later loosened to 8 +# once profiling showed headroom) - this board never got the equivalent +# cap despite also building large C++ codebases. +BR2_JLEVEL=8 + # GPU - Mali-G52 blob (hardkernel). The kernel also builds Panfrost as an # unused module (matches the real device: loaded, refcount 0) but we use # the proprietary blob, matching what's verified working on hardware. From 2d1a8702470fd12553b153e842460f78b1129f84 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Mon, 10 Aug 2026 10:04:09 +0900 Subject: [PATCH 004/110] local.mk.example: fix KERNEL_SRCDIR path after SSD migration ~/git_projects no longer exists (2026-08-10 SSD swap moved everything to /mnt/2026_990pro_2tb_1_data/git_projects/, no symlink recreated on purpose per the migration handoff). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../rockchip/bsp-odroidm1/local.mk.example | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 board/batocera/rockchip/bsp-odroidm1/local.mk.example diff --git a/board/batocera/rockchip/bsp-odroidm1/local.mk.example b/board/batocera/rockchip/bsp-odroidm1/local.mk.example new file mode 100644 index 00000000000..9ca9dfe90ff --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/local.mk.example @@ -0,0 +1,34 @@ +# Copy (or symlink) this file to output/rk3568-odroidm1/local.mk to build +# the kernel straight from a live local checkout instead of downloading +# Pyohwan/linux's panfrost-enable branch - skips buildroot's +# download/extract steps entirely. Edit the kernel source, then +# `make linux-rebuild all`, no commit/push needed per iteration. +# +# Requires the matching docker mount: pass KERNEL_SRCDIR= on every `make rk3568-odroidm1-build` +# invocation (see docker/docker.mk's KERNEL_SRCDIR opt-in) - the container +# only sees the checkout at /kernel-src if that variable is set, e.g.: +# +# make rk3568-odroidm1-build BATCH_MODE=1 KERNEL_SRCDIR=/mnt/2026_990pro_2tb_1_data/git_projects/linux-odroidm1 +# +# Why this exists: buildroot's git-download cache keys its dl/ tarball +# filename off BR2_LINUX_KERNEL_CUSTOM_REPO_VERSION's literal string, not +# off the commit it resolves to. A branch name never changes that string, +# so pushing new commits to an already-downloaded branch is invisible to +# buildroot forever - this cost two full iterations of the odroid-m1- +# panfrost debugging cycle silently rebuilding the same stale commit +# (see Joplin "B트랙 buildroot 커널 캐시 오염" note, 2026-08-08). This +# OVERRIDE_SRCDIR mechanism is buildroot's own documented answer for +# "actively editing source, rebuilding often" (manual: "Using Buildroot +# during development") - it bypasses the download cache instead of +# fighting it. +# +# Caveat: while this is active, the working tree - not git history - is +# what's actually being built. Check `git status`/`git stash list` in the +# kernel repo before trusting what a given image contains, and commit +# anything worth keeping. Once a build is verified good enough to archive, +# delete/rename this file and go back to pinning +# BR2_LINUX_KERNEL_CUSTOM_REPO_VERSION to that commit's full hash in the +# .board file instead - the two mechanisms are meant to be swapped between, +# not run at the same time. +LINUX_OVERRIDE_SRCDIR = /kernel-src From 71b457003a3f7eec8a741036308d6290827c3e03 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Wed, 12 Aug 2026 22:38:44 +0900 Subject: [PATCH 005/110] local.mk.example: fix stale panfrost-enable branch reference for track C Cherry-picked from odroid-m1-panfrost (B) where the comment correctly named that branch's kernel repo version. Track C stays on A's BSP kernel branch (fix-hdmi-dsi-dual-livelock), not panfrost-enable. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- board/batocera/rockchip/bsp-odroidm1/local.mk.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/board/batocera/rockchip/bsp-odroidm1/local.mk.example b/board/batocera/rockchip/bsp-odroidm1/local.mk.example index 9ca9dfe90ff..555d81e1799 100644 --- a/board/batocera/rockchip/bsp-odroidm1/local.mk.example +++ b/board/batocera/rockchip/bsp-odroidm1/local.mk.example @@ -1,6 +1,8 @@ # Copy (or symlink) this file to output/rk3568-odroidm1/local.mk to build # the kernel straight from a live local checkout instead of downloading -# Pyohwan/linux's panfrost-enable branch - skips buildroot's +# Pyohwan/linux's fix-hdmi-dsi-dual-livelock branch (BSP 6.1, same kernel +# as A/B - track C doesn't change kernel branch, see project memory +# project_odroid_retro_track_c_v3.md §7) - skips buildroot's # download/extract steps entirely. Edit the kernel source, then # `make linux-rebuild all`, no commit/push needed per iteration. # From 9013dd3096f6af063d0ef92837a651bc5127376f Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 18:00:06 +0900 Subject: [PATCH 006/110] configgen: inject GALLIUM_HUD=fps whenever show_fps is on, as a Mesa-side graph layered on top of each generator's existing fps display Single injection point (right where hud_support already sets MANGOHUD_DLSYM/MANGOHUD_CONFIGFILE) instead of touching every generator file individually - GALLIUM_HUD is a plain env var Mesa's gallium drivers read at process start, so it's a no-op on anything that never loads a gallium GL/GLES context (non-GL systems, the vendor Mali blob, older Mesa without HUD support). That makes it a natural fallback to each generator's own show_fps wiring (retroarch's fps_show, dolphin's ShowFPS, etc.) rather than requiring an either/or branch per emulator. Surfaced this session testing odroid-m1's Panfrost track: no numeric GPU-load metric is exposed anywhere on this kernel (no devfreq node - the GPU clock is statically pinned - no debugfs, and gallium_hud itself rejects the "GPU-load" query for this driver: "unknown driver query 'GPU-load'"), but the graph-based fps/frametime view was still strictly better than the existing plain-number counter for judging real-time performance during A/B testing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../configgen/configgen/emulatorlauncher.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py index ceaf8fe2a40..9c3f0d8f330 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py @@ -207,6 +207,20 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r if not generator.hasInternalMangoHUDCall(): cmd.array.insert(0, "mangohud") + # Mesa's own on-screen graph (fps/frametime) via the gallium + # driver, set unconditionally alongside whatever each + # generator's own show_fps wiring already does (native + # counters, e.g. retroarch's fps_show or dolphin's + # ShowFPS - left untouched, one central env var here beats + # touching every generator individually). Harmless no-op + # on processes that never load a gallium driver (any + # non-GL system, or the vendor Mali blob) or on older + # Mesa without HUD support - the process just never draws + # it, so this is naturally a fallback to each generator's + # existing fps display instead of an either/or choice. + if system.config.show_fps: + cmd.env["GALLIUM_HUD"] = "fps" + # generate the gun help try: default_gun_help_dir = Path("/var/run/batocera-overlays") From eb86551f4ed4485db057c2a907180e68e5926b36 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 22:14:52 +0900 Subject: [PATCH 007/110] odroidm1: default global.powermode to highperformance Same change as odroid-m1-panfrost - global.powermode was never actually set in this board's default batocera.conf, so every A/B track fps measurement so far ran under batocera-power-mode's unset-fallback (set_default), not highperformance. Factory default only; ES's Game Settings > Power Mode menu still overrides it live. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../sysconfigs/rk3568/batocera.conf.Hardkernel_ODROID_M1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package/batocera/core/batocera-system/sysconfigs/rk3568/batocera.conf.Hardkernel_ODROID_M1 b/package/batocera/core/batocera-system/sysconfigs/rk3568/batocera.conf.Hardkernel_ODROID_M1 index bf2284aabe0..8bd01ef559a 100644 --- a/package/batocera/core/batocera-system/sysconfigs/rk3568/batocera.conf.Hardkernel_ODROID_M1 +++ b/package/batocera/core/batocera-system/sysconfigs/rk3568/batocera.conf.Hardkernel_ODROID_M1 @@ -39,6 +39,15 @@ system.cpu.governor=interactive # powermode_launch_hooks.sh), defeating the point of a single global # knob. See Joplin "ODROID-M1 retro OS 빌드 계획" for the underlying # measurements if per-system defaults are ever wanted again. +# +# global.powermode itself, however, was never actually set here - every +# A/B track fps measurement so far ran under whatever batocera-power-mode +# falls back to when global.powermode is unset (`set_default`, EPP/governor +# "default", not "performance"), not the highperformance path the above +# measurements were based on. Setting it here changes only the factory +# default; ES's Game Settings > Power Mode menu still overrides it live +# same as before. +global.powermode=highperformance # Default core for the fbneo system: dsno/Zansword's Korean-patched # fork (package/batocera/emulators/retroarch/libretro/ From d85e9984428dc6d93cc9f3e9af8280c872693b50 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 19:11:33 +0900 Subject: [PATCH 008/110] Fix vkquake2/pcsx2 desktop-GL blockers for GLES-only Vulkan build vkquake2 genuinely builds ref_gl.so and ref_vk.so side by side (real dual-renderer engine), so batocera-system's auto-select for it needs BR2_PACKAGE_HAS_LIBGL as well as BATOCERA_VULKAN/XORG7 - false on this board (Panfrost only exposes GLES/EGL). A defconfig "is not set" line in the board file can't override a forced Kconfig `select`, so this had to be fixed at the actual select condition, confirmed via a failed build ("libgl is in the dependency chain of libglu... Stop."). pcsx2's WXWIDGETS select turned out to be unrelated dead weight - its .mk never references wx in any CMake option (OpenGL/Vulkan are already properly gated on HAS_LIBGL/BATOCERA_VULKAN), so gated the select and the .mk dependency on HAS_LIBGL too, matching how pcsx2 actually gates its own optional GL support. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- package/batocera/core/batocera-system/Config.in | 2 +- package/batocera/emulators/pcsx2/Config.in | 6 +++--- package/batocera/emulators/pcsx2/pcsx2.mk | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/package/batocera/core/batocera-system/Config.in b/package/batocera/core/batocera-system/Config.in index 9d94c8ca360..fc04f6ca05d 100644 --- a/package/batocera/core/batocera-system/Config.in +++ b/package/batocera/core/batocera-system/Config.in @@ -1202,7 +1202,7 @@ config BR2_PACKAGE_BATOCERA_PORTS_SYSTEMS # Quake 2 select BR2_PACKAGE_LIBRETRO_VITAQUAKE2 if BR2_PACKAGE_BATOCERA_GLES3 - select BR2_PACKAGE_VKQUAKE2 if BR2_PACKAGE_BATOCERA_VULKAN && BR2_PACKAGE_XORG7 + select BR2_PACKAGE_VKQUAKE2 if BR2_PACKAGE_BATOCERA_VULKAN && BR2_PACKAGE_XORG7 && BR2_PACKAGE_HAS_LIBGL select BR2_PACKAGE_YQUAKE2 # ALL select BR2_PACKAGE_YQUAKE2_XATRIX if BR2_PACKAGE_YQUAKE2 # Mission Packs select BR2_PACKAGE_YQUAKE2_ROGUE if BR2_PACKAGE_YQUAKE2 diff --git a/package/batocera/emulators/pcsx2/Config.in b/package/batocera/emulators/pcsx2/Config.in index 314191b0e97..dfac3b7e46e 100644 --- a/package/batocera/emulators/pcsx2/Config.in +++ b/package/batocera/emulators/pcsx2/Config.in @@ -24,9 +24,9 @@ config BR2_PACKAGE_PCSX2 select BR2_PACKAGE_SHADERC select BR2_PACKAGE_VULKAN_HEADERS if BR2_PACKAGE_BATOCERA_VULKAN select BR2_PACKAGE_VULKAN_LOADER if BR2_PACKAGE_BATOCERA_VULKAN - select BR2_PACKAGE_WEBP - select BR2_PACKAGE_WXWIDGETS - select BR2_PACKAGE_YAML_CPP + select BR2_PACKAGE_WEBP + select BR2_PACKAGE_WXWIDGETS if BR2_PACKAGE_HAS_LIBGL + select BR2_PACKAGE_YAML_CPP select BR2_PACKAGE_ZLIB help PCSX2 is a free and open-source PlayStation 2 (PS2) emulator. diff --git a/package/batocera/emulators/pcsx2/pcsx2.mk b/package/batocera/emulators/pcsx2/pcsx2.mk index 3bb6e9bdd6a..cb17adb011d 100644 --- a/package/batocera/emulators/pcsx2/pcsx2.mk +++ b/package/batocera/emulators/pcsx2/pcsx2.mk @@ -24,7 +24,11 @@ PCSX2_SUPPORTS_IN_SOURCE_BUILD = NO PCSX2_DEPENDENCIES += alsa-lib ecm fmt freetype host-clang host-libcurl kddockwidgets PCSX2_DEPENDENCIES += libaio libbacktrace libcurl libgtk3 libpcap libpng libsamplerate PCSX2_DEPENDENCIES += libsoundtouch plutosvg portaudio qt6base qt6svg qt6tools -PCSX2_DEPENDENCIES += rapidyaml shaderc sdl3 webp wxwidgets xorgproto yaml-cpp zlib +PCSX2_DEPENDENCIES += rapidyaml shaderc sdl3 webp xorgproto yaml-cpp zlib + +ifeq ($(BR2_PACKAGE_HAS_LIBGL),y) +PCSX2_DEPENDENCIES += wxwidgets +endif # Use clang for performance PCSX2_CONF_OPTS += -DCMAKE_C_COMPILER=$(HOST_DIR)/bin/clang From bf09efaf9c3c4a285913fcc62745f57ac4bf9f5a Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 20:20:32 +0900 Subject: [PATCH 009/110] Add RK3568 board branch to libretro-parallel-n64 - fixes x86 SSE build failure odroidm1/rk3568 wasn't in upstream's per-board platform/ARCH table, so it fell through to the generic default where the Makefile's own ARCH fallback (uname -m) picks up the build host's x86_64 arch instead of the cross-compile target, pulling in xbyak's x86-only cpuid.h unconditionally (confirmed via a real build failure: "-msse"/"-msse2" rejected by aarch64-buildroot-linux-gnu-g++ compiling gles2rice). Added an explicit branch mirroring RK3588/RK3576/H5 (WITH_DYNAREC=aarch64), reusing H5's CPUFLAGS (armv8-a+crc, cortex-a53 tuning) since it's a safe baseline subset for RK3568's Cortex-A55 cores with no extension-gated instructions. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../libretro-parallel-n64.mk | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package/batocera/emulators/retroarch/libretro/libretro-parallel-n64/libretro-parallel-n64.mk b/package/batocera/emulators/retroarch/libretro/libretro-parallel-n64/libretro-parallel-n64.mk index 3e253dbdb12..60010374c01 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-parallel-n64/libretro-parallel-n64.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-parallel-n64/libretro-parallel-n64.mk @@ -32,6 +32,21 @@ LIBRETRO_PARALLEL_N64_PLATFORM=rpi2 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_XU4),y) LIBRETRO_PARALLEL_N64_PLATFORM=odroid LIBRETRO_PARALLEL_N64_BOARD=ODROID-XU4 +else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3568),y) +# Not in upstream's own per-board platform table (odroidm1/rk3568 is a +# batocera-only target). Falling through to the generic "else" branch +# below would leave $(ARCH) unset, and the upstream Makefile's fallback +# `ARCH = $(shell uname -m)` then picks up the *build host's* arch +# (x86_64, since this runs inside the cross-compile docker container) - +# confirmed via a real build failure (xbyak's x86-only cpuid.h pulled in, +# "-msse"/"-msse2" rejected by aarch64-buildroot-linux-gnu-g++). Reusing +# the H5 platform's CPUFLAGS (-march=armv8-a+crc, cortex-a53 tuning, no +# crypto/dotprod extensions) rather than adding new SoC-specific flags - +# it's a strict baseline armv8-a subset with no extension-gated +# instructions, so it's safe to run on RK3568's Cortex-A55 cores too even +# though the tuning target isn't an exact match. +LIBRETRO_PARALLEL_N64_EXTRA_ARGS+=WITH_DYNAREC=aarch64 +LIBRETRO_PARALLEL_N64_PLATFORM=h5 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3576),y) LIBRETRO_PARALLEL_N64_EXTRA_ARGS+=WITH_DYNAREC=aarch64 LIBRETRO_PARALLEL_N64_PLATFORM=rk3576 From b995fdd7c7517b8d9939192153fad1d8791bfb39 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Tue, 11 Aug 2026 21:49:04 +0900 Subject: [PATCH 010/110] Enable real PPSSPP Vulkan on RK3568 - was force-disabled via ARM_NO_VULKAN ppsspp.mk only flips ARM_NO_VULKAN=OFF (i.e. actually allows Vulkan) for RPi4/5; every other ARM board gets ARM_NO_VULKAN=ON regardless of the general -DVULKAN=ON flag, silently compiling Vulkan support back out. Confirmed via the built binary having zero Vulkan symbols despite BATOCERA_VULKAN=y. Added RK3568 to the same allowlist as RPi4/5. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- package/batocera/emulators/ppsspp/ppsspp.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/batocera/emulators/ppsspp/ppsspp.mk b/package/batocera/emulators/ppsspp/ppsspp.mk index bd784aeed81..5190a9d41d0 100644 --- a/package/batocera/emulators/ppsspp/ppsspp.mk +++ b/package/batocera/emulators/ppsspp/ppsspp.mk @@ -83,7 +83,7 @@ ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN),y) PPSSPP_TARGET_CFLAGS += -DEGL_NO_X11=1 -DMESA_EGL_NO_X11_HEADERS=1 endif # rpi4/5 vulkan support - ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BCM2711)$(BR2_PACKAGE_BATOCERA_TARGET_BCM2712),y) + ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BCM2711)$(BR2_PACKAGE_BATOCERA_TARGET_BCM2712)$(BR2_PACKAGE_BATOCERA_TARGET_RK3568),y) PPSSPP_CONF_OPTS += -DARM_NO_VULKAN=OFF else ifeq ($(BR2_arm)$(BR2_aarch64),y) PPSSPP_CONF_OPTS += -DARM_NO_VULKAN=ON From f076d7057434178bf4bc7122ede70c06733e18c8 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Wed, 12 Aug 2026 21:39:19 +0900 Subject: [PATCH 011/110] pcsx2: fix ps2 default emulator + eglfs for no-compositor boards configgen-defaults-rk3568.yml still defaulted ps2 to libretro/play (a stale upstream default from before PCSX2 was viable on aarch64) - our board only registers the standalone pcsx2 emulator in es_systems.yml, so this stale default made every PS2 launch try a nonexistent libretro core and crash immediately ("Frontend is built for dynamic libretro cores, but path is not set"). Confirmed live and fixed by pointing the default at pcsx2/pcsx2 instead. Separately, pcsx2-qt itself always defaulted to the xcb Qt platform plugin, which aborts outright on this board (no X11, no compositor - same failure class documented in duckstationGenerator.py for duckstation-qt/melonDS). Reused the exact same WAYLAND_DISPLAY/DISPLAY detection -> eglfs fallback pattern. Confirmed live: PCSX2 now reaches its own BIOS-missing dialog instead of crashing at Qt init. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../configgen/generators/pcsx2/pcsx2Generator.py | 16 +++++++++++++++- .../configs/configgen-defaults-rk3568.yml | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py index 71fabacf7e3..7db84ee111f 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py @@ -4,6 +4,7 @@ import re import shutil import time +from os import environ from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Final @@ -120,8 +121,21 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game if not re.search(r'^flags\s*:.*\ssse4_1\W', cpuinfo.read(), re.MULTILINE): _logger.warning("CPU does not support SSE4.1 which is required by pcsx2. The emulator will likely crash with SIGILL (illegal instruction).") + # check if we're running wayland or X11 - boards with neither (no + # compositor, direct KMS/DRM like odroid-m1) need eglfs instead, + # otherwise Qt defaults into xcb with no X server to talk to and + # aborts outright (same failure class as standalone duckstation-qt/ + # melonDS, see duckstationGenerator.py) + if environ.get("WAYLAND_DISPLAY"): + qt_qpa_platform = "wayland" + elif environ.get("DISPLAY"): + qt_qpa_platform = "xcb" + else: + qt_qpa_platform = "eglfs" + envcmd: dict[str, str | Path] = { - "XDG_CONFIG_HOME": CONFIGS + "XDG_CONFIG_HOME": CONFIGS, + "QT_QPA_PLATFORM": qt_qpa_platform } # wheels won't work correctly when SDL_GAMECONTROLLERCONFIG is set. excluding wheels from SDL_GAMECONTROLLERCONFIG doesn't fix too. diff --git a/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568.yml b/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568.yml index 24e13533f1d..6db8b8f0236 100644 --- a/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568.yml +++ b/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568.yml @@ -39,8 +39,8 @@ systemsp: options: hud_support: false # flycast core, same issue as dreamcast above ps2: - emulator: libretro - core: play + emulator: pcsx2 + core: pcsx2 flash: emulator: lightspark core: lightspark From 9af63f47c17951e192a74e6085f0c5fb33535220 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Wed, 12 Aug 2026 22:09:31 +0900 Subject: [PATCH 012/110] dolphin: eglfs for no-compositor boards, same pattern as duckstation/pcsx2 dolphin-emu (standalone) always defaulted to the xcb Qt platform plugin, aborting outright on this board (no X11, no compositor). Confirmed live that eglfs actually works for Dolphin (unlike pcsx2-qt, which hits a separate "Failed to get window info from widget" Qt/eglfs multi-widget limitation) - Dolphin ran a real GPU-rendered session via libqeglfs-kms-integration.so + libgallium for 90+ seconds with no crash. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../generators/dolphin/dolphinGenerator.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py index 1705db84009..8b466e4e2e9 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py @@ -429,12 +429,25 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game if state_filename := system.config.get('state_filename'): commandArray.extend(["--save_state", state_filename]) + # check if we're running wayland or X11 - boards with neither (no + # compositor, direct KMS/DRM like odroid-m1) need eglfs instead, + # otherwise Qt defaults into xcb with no X server to talk to and + # aborts outright (same failure class as standalone duckstation-qt/ + # pcsx2-qt, see duckstationGenerator.py/pcsx2Generator.py) + if environ.get("WAYLAND_DISPLAY"): + qt_qpa_platform = "wayland" + elif environ.get("DISPLAY"): + qt_qpa_platform = "xcb" + else: + qt_qpa_platform = "eglfs" + return Command.Command( array=commandArray, env={ "XDG_CONFIG_HOME": CONFIGS, "XDG_DATA_HOME": SAVES, - "XDG_CACHE_HOME": CACHE + "XDG_CACHE_HOME": CACHE, + "QT_QPA_PLATFORM": qt_qpa_platform } ) From 6981a3fdbe07bdbada7cb4867d93496fa577319c Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Wed, 12 Aug 2026 22:40:27 +0900 Subject: [PATCH 013/110] odroidm1: enable PCSX2/Dolphin-standalone/parallel-n64/azahar/ppsspp for track C Board-file portion of B's "Enable PanVK + Vulkan-capable systems" (1246f4761d), re-applied manually with the PanVK/BATOCERA_VULKAN lines dropped - track C's Vulkan comes from the g29p1 blob + hand-written ICD, not a Mesa driver Kconfig select. flycastvl/ps2/play intentionally not re-added (already dead ends on B, see 04c0e462f6/5f1cf74e76). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 34444abe796..7c8d5cf713e 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -128,6 +128,41 @@ BR2_PACKAGE_LIBRETRO_FBNEO_KOREAN=y # a Software render mode that doesn't need GL at all, worth trying. BR2_PACKAGE_LIBRETRO_MELONDS_DS=y +# Track C (g29p1 Vulkan blob + Wayland, see project memory +# project_odroid_retro_track_c_v3.md) systems/cores. No BATOCERA_VULKAN/ +# MESA3D_VULKAN_DRIVER_* line here - unlike B's PanVK, this board's Vulkan +# comes from the g29p1 blob itself (mali.json ICD written by hand once the +# blob is swapped in, not a buildroot-selectable Mesa driver) - so nothing +# to select at the Kconfig level for that part, it's a runtime/package +# addition to mali-g52-odroidm1.mk once gate 1/2 pass. These system/core +# enables are GPU-driver-independent and were already confirmed to build +# on B (cherry-picked selects only, not the PanVK-specific lines): +# +# PCSX2 (PS2) - never tested on this board before B. Needs BR2_PACKAGE_ +# HAS_LIBGL fixed above (see vkquake2/pcsx2 desktop-GL commit) to avoid +# pulling in wxwidgets/libglu on this GLES-only board. +BR2_PACKAGE_PCSX2=y + +# Dolphin standalone (GameCube/Wii) - distinct from libretro-dolphin above +# (GL only). Has its own Vulkan gfxbackend option once g29p1 is in. +BR2_PACKAGE_DOLPHIN_EMU=y + +# ParaLLEl-RDP N64 core - Vulkan-only renderer, needs the RK3568 arch +# branch fix (see libretro-parallel-n64.mk commit) to cross-compile aarch64. +BR2_PACKAGE_LIBRETRO_PARALLEL_N64=y + +# libretro Azahar (3DS) - no 3DS system was buildable on this board before. +BR2_PACKAGE_LIBRETRO_AZAHAR=y + +# libretro PPSSPP core - distinct build from the standalone PPSSPP (already +# on by default). Standalone's own Vulkan gfxbackend needs the RK3568 +# allowlist fix (see ppsspp.mk ARM_NO_VULKAN commit). +BR2_PACKAGE_LIBRETRO_PPSSPP=y + +# Dropped on B, not re-added here: libretro-flycastvl (stale fork, no ARM +# platform wiring), libretro-ps2/libretro-play (CMake arch-detection +# failure / not in es_systems.yml). See B's commits 04c0e462f6/5f1cf74e76. + # Firmware BR2_PACKAGE_FIRMWARE_ARMBIAN=y From 1ae087df61edaf8e623a1feaf5ecb67e92d5282a Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Thu, 13 Aug 2026 09:02:43 +0900 Subject: [PATCH 014/110] odroidm1: reintroduce labwc for track C, fix stale gbm_bo_get_fd alias BR2_PACKAGE_BATOCERA_WAYLAND_LABWC=y: track C's blob Vulkan has no KMSDRM-direct display path (VK_KHR_display fails the same way under PanVK on B), so Wayland is back on the table after A/B dropped it. mali-g52-odroidm1.mk: the gbm_bo_get_fd_for_plane compat shim assumed the blob only ever exports the pre-standardization gbm_bo_get_fd_per_ plane name (true for g13p0) and unconditionally aliased the modern name to it based on grepping the vendor header text. g24p0's libmali.so.1.9.0 already exports gbm_bo_get_fd_for_plane directly - the header text just never caught up - so the alias pointed wlroots (the first real caller of this symbol on this board) at a symbol that no longer exists, breaking the link. Check the .so's actual exported symbols instead of trusting the header. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 8 +++++- .../mali-g52-odroidm1/mali-g52-odroidm1.mk | 28 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 7c8d5cf713e..77c773a05b2 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -79,7 +79,13 @@ BR2_PACKAGE_MALI_G52_ODROIDM1=y # supportSystemRotation, which batocera-resolution.drm reports as # unsupported). Simultaneous dual-output (HDMI+VU8M) is lost, but not # needed - single active output at a time is enough. -# BR2_PACKAGE_BATOCERA_WAYLAND_LABWC is not set +# Track C reintroduces labwc, reversing A/B's GBM/KMSDRM-direct decision +# above: gate 1 (g29p1 blob accepted by vendor kbase, 2026-08-12) passed, +# and blob Vulkan's only display path is VK_KHR_display, which PanVK also +# has and still fails to create a surface with on this board (B, 2026-08- +# 11) - Wayland is the only Vulkan output path left to try. See +# project_odroid_retro_track_c_v3.md for the full gate 1/2 rationale. +BR2_PACKAGE_BATOCERA_WAYLAND_LABWC=y # libcurl was previously built with brotli support because BR2_PACKAGE_BROTLI # got pulled in transitively via webkitgtk -> woff2 -> brotli, and webkitgtk diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk index 5b6bbefe2c9..79286404d85 100644 --- a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk +++ b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk @@ -111,19 +111,33 @@ define MALI_G52_ODROIDM1_INSTALL_STAGING_CMDS # eglplatform.h logic, so patch its staged copy the same way. grep -q MESA_EGL_NO_X11_HEADERS $(STAGING_DIR)/usr/include/EGL/eglplatform.h || \ sed -i '1a #define MESA_EGL_NO_X11_HEADERS 1' $(STAGING_DIR)/usr/include/EGL/eglplatform.h - # This vendor gbm.h (2022-03 snapshot) predates upstream Mesa's standard - # name for per-plane DMA-BUF export: it only declares the fd itself as - # gbm_bo_get_fd_per_plane (confirmed still exported under that name by - # nm -D on libmali.so.1.9.0), not the gbm_bo_get_fd_for_plane name - # wlroots >= 0.18 actually calls. Same function/signature, alias it. + # This vendor gbm.h never declares gbm_bo_get_fd_for_plane, the name + # upstream Mesa standardized on and wlroots >= 0.18 actually calls - + # g13p0-era blobs only exported the pre-standardization name + # (gbm_bo_get_fd_per_plane) under the hood, so this used to always + # inject a declare-and-alias-the-old-name macro. g24p0's + # libmali.so.1.9.0 has since started exporting gbm_bo_get_fd_for_plane + # directly (confirmed via nm -D) - the vendor's header text just never + # caught up - so aliasing to per_plane unconditionally now points at a + # symbol that no longer exists in the .so, breaking the link instead of + # fixing it (invisible until wlroots, the first real caller of this + # symbol on this board, failed with "undefined reference to + # gbm_bo_get_fd_per_plane"). Check the actual exported symbol table so + # this keeps working across blob versions either way: alias only if the + # .so genuinely lacks the modern name, otherwise just declare it (the + # .so already has it, only the header prototype is missing). # Insert right before the closing "#ifdef __cplusplus / }" block (the # second occurrence) so it stays inside both extern "C" and the # include guard, instead of a blind end-of-file append. if ! grep -q gbm_bo_get_fd_for_plane $(STAGING_DIR)/usr/include/gbm.h; then \ + if nm -D $(STAGING_DIR)/usr/lib/libmali.so.1.9.0 | grep -q ' T gbm_bo_get_fd_for_plane$$'; then \ + DECL='int\ngbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);\n\n'; \ + else \ + DECL='int\ngbm_bo_get_fd_per_plane(struct gbm_bo *bo, int plane);\n#define gbm_bo_get_fd_for_plane gbm_bo_get_fd_per_plane\n\n'; \ + fi; \ LINE=$$(grep -n '^#ifdef __cplusplus' $(STAGING_DIR)/usr/include/gbm.h | tail -1 | cut -d: -f1); \ head -n $$((LINE-1)) $(STAGING_DIR)/usr/include/gbm.h > $(STAGING_DIR)/usr/include/gbm.h.new; \ - printf 'int\ngbm_bo_get_fd_per_plane(struct gbm_bo *bo, int plane);\n#define gbm_bo_get_fd_for_plane gbm_bo_get_fd_per_plane\n\n' \ - >> $(STAGING_DIR)/usr/include/gbm.h.new; \ + printf "$$DECL" >> $(STAGING_DIR)/usr/include/gbm.h.new; \ tail -n +$$LINE $(STAGING_DIR)/usr/include/gbm.h >> $(STAGING_DIR)/usr/include/gbm.h.new; \ mv $(STAGING_DIR)/usr/include/gbm.h.new $(STAGING_DIR)/usr/include/gbm.h; \ fi From c12e978c3da096368687ea12d3e2b701e673a83f Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Thu, 13 Aug 2026 09:04:53 +0900 Subject: [PATCH 015/110] mali-g52-odroidm1: swap blob to g29p1 for track C Gate 1 (2026-08-12) confirmed g29p1 works against our unchanged vendor kbase (g25p0): GLES survives, vulkaninfo enumerates Mali-G52. Source moves from the "libmali" branch (g24p0) to "libmali-next" (g29p1 isn't published on the old branch), same JeffyCN mirrors repo, pinned commit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../mali-g52-odroidm1/mali-g52-odroidm1.hash | 1 + .../mali-g52-odroidm1/mali-g52-odroidm1.mk | 40 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.hash b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.hash index c64da1bde51..558af90c6a3 100644 --- a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.hash +++ b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.hash @@ -1,3 +1,4 @@ sha256 c45656ccd629c256bdafb1b4f3034516873d52ecfafdb9f0b392442fe9bb3ffe libmali-bifrost-g52-g13p0-wayland-gbm_1.9-5+202604211715~resolute_arm64.deb sha256 8b7bd1f969e778459d79a51e5f58c26eda0b818580966daba16ee2fc08f4c151 libmali-ad4c28932c3d07c75fc41dd4a3333f9013a25e7f.tar.gz sha256 15f94e34138ab315daab8d90b2bd17b792dc32d0b2206c6cc2efc670eff5f93d libmali-bifrost-g52-g24p0-wayland-gbm.so +sha256 605e3a5caf1be62bb48d97b1168434257355d56db5e3be9b515cfa742c173118 libmali-bifrost-g52-g29p1.so diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk index 79286404d85..2e0656345ff 100644 --- a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk +++ b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk @@ -3,28 +3,24 @@ # mali-g52-odroidm1 # ################################################################################ -# Userspace blob is g24p0 (DDK generation newer than the g13p0 the hardkernel -# BSP kernel's kbase was originally paired with) - live-tested on real -# hardware by swapping just this file against our unchanged 6.1 kernel/kbase -# first: no kbase UK-ABI mismatch, ES/EGL init and rendering all fine. Picked -# over the LinuxFactory-distributed g13p0 .deb (still the peripheral-files -# source below - OpenCL vendor JSON/profile.d, unrelated to blob version) for -# the extra EGL 1.5 surface it exposes (eglGetPlatformDisplay natively, plus -# EGL_EXT_platform_base/EGL_KHR_platform_gbm/EGL_ANDROID_native_fence_sync -# strings confirmed present via nm/strings) - g13p0 lacks these, which is -# what excluded moonlight-qt from this board's build and is suspected (not -# confirmed) as part of the mangohud/SDL_CreateWindow EGL interaction bug. -# Source: https://github.com/JeffyCN/mirrors, "libmali" branch - the -# Rockchip-community-standard libmali distribution (other mirrors sync from -# it). Pinned to a specific commit rather than the branch HEAD for -# reproducible builds. -MALI_G52_ODROIDM1_G24P0_COMMIT = 077bc8f582c5da0cb80136a711ce3be2fa1a04cd -MALI_G52_ODROIDM1_G24P0_SOURCE = libmali-bifrost-g52-g24p0-wayland-gbm.so -MALI_G52_ODROIDM1_G24P0_SITE = https://raw.githubusercontent.com/JeffyCN/mirrors/$(MALI_G52_ODROIDM1_G24P0_COMMIT)/lib/aarch64-linux-gnu +# Track C: userspace blob is g29p1, the unified GLES+Vulkan build (DDK +# generation newer than g24p0, which A/B use) - gate 1 (2026-08-12) live- +# swapped just this file on an A image against our unchanged 6.1 kernel/ +# kbase: no UK-ABI mismatch, GLES survived (ES System Info showed real +# GLSL ES 3.20/GL_VERSION strings), and vulkaninfo enumerated the GPU +# ("Mali-G52", DRIVER_ID_ARM_PROPRIETARY) - vendor g25p0 kbase accepts +# g29p1 userspace as-is, no out-of-tree kbase module needed (see +# project_odroid_retro_track_c_v3.md gate 1/light-path). Source: +# https://github.com/JeffyCN/mirrors, "libmali-next" branch (not "libmali" +# - g29p1 isn't published on the branch g24p0 used). Pinned to a specific +# commit rather than the branch HEAD for reproducible builds. +MALI_G52_ODROIDM1_G29P1_COMMIT = 4233031d818e97a19e8a9cdbbd5c15795ededd93 +MALI_G52_ODROIDM1_G29P1_SOURCE = libmali-bifrost-g52-g29p1.so +MALI_G52_ODROIDM1_G29P1_SITE = https://raw.githubusercontent.com/JeffyCN/mirrors/$(MALI_G52_ODROIDM1_G29P1_COMMIT)/lib/aarch64-linux-gnu # Peripheral files only (OpenCL vendor ICD JSON, profile.d, copyright) still # come from the LinuxFactory g13p0 .deb - the actual libmali.so.1.9.0 binary -# extracted from it gets overwritten with the g24p0 blob above in +# extracted from it gets overwritten with the g29p1 blob above in # EXTRACT_CMDS, same live-tested swap, just made to persist across rebuilds. MALI_G52_ODROIDM1_VERSION = 1.9-5+202604211715~resolute MALI_G52_ODROIDM1_SOURCE = libmali-bifrost-g52-g13p0-wayland-gbm_$(MALI_G52_ODROIDM1_VERSION)_arm64.deb @@ -45,7 +41,7 @@ MALI_G52_ODROIDM1_DEPENDENCIES = libdrm wayland mesa3d-headers # doesn't ship. MALI_G52_ODROIDM1_HEADERS_VERSION = ad4c28932c3d07c75fc41dd4a3333f9013a25e7f MALI_G52_ODROIDM1_EXTRA_DOWNLOADS = https://github.com/batocera-linux/rockchip-packages/releases/download/20220303/libmali-$(MALI_G52_ODROIDM1_HEADERS_VERSION).tar.gz -MALI_G52_ODROIDM1_EXTRA_DOWNLOADS += $(MALI_G52_ODROIDM1_G24P0_SITE)/$(MALI_G52_ODROIDM1_G24P0_SOURCE) +MALI_G52_ODROIDM1_EXTRA_DOWNLOADS += $(MALI_G52_ODROIDM1_G29P1_SITE)/$(MALI_G52_ODROIDM1_G29P1_SOURCE) define MALI_G52_ODROIDM1_EXTRACT_CMDS $(AR) --output=$(@D) -x $(MALI_G52_ODROIDM1_DL_DIR)/$(MALI_G52_ODROIDM1_SOURCE) @@ -73,11 +69,11 @@ define MALI_G52_ODROIDM1_EXTRACT_CMDS # convention this file is essentially a placeholder (GLES3 extensions # are declared in GLES2/gl2ext.h instead) - ship that standard content. cp $(MALI_G52_ODROIDM1_PKGDIR)/gl3ext.h $(@D)/usr/include/GLES3/gl3ext.h - # Overwrite the g13p0 binary the .deb shipped with the g24p0 blob - + # Overwrite the g13p0 binary the .deb shipped with the g29p1 blob - # same filename (libmali.so.1.9.0), so every symlink/pkgconfig/ # ld.so.preload path below still applies unchanged. Live-tested this # exact swap on real hardware first (see header comment). - cp $(MALI_G52_ODROIDM1_DL_DIR)/$(MALI_G52_ODROIDM1_G24P0_SOURCE) \ + cp $(MALI_G52_ODROIDM1_DL_DIR)/$(MALI_G52_ODROIDM1_G29P1_SOURCE) \ $(@D)/usr/lib/aarch64-linux-gnu/libmali.so.1.9.0 # On a real dpkg system these top-level sonames get created by # update-alternatives at install time (nothing in this .deb's own From cb3030395520777e5ddc430df83e14f387dfc59f Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Thu, 13 Aug 2026 10:49:43 +0900 Subject: [PATCH 016/110] odroidm1: disable libgtk3's Wayland GDK backend on track C pcsx2 pulls in libgtk3 unconditionally, and libgtk3's Wayland backend defaults to on once BR2_PACKAGE_WAYLAND is present (true now that labwc is back). g29p1 doesn't export wl_egl_window_create/resize/destroy - confirmed via nm -D, unlike eglGetPlatformDisplay/eglInitialize/ eglCreatePlatformWindowSurface which it does have - so the link fails. X11 stays available via BR2_PACKAGE_XORG7, this only drops GTK's Wayland-specific GDK backend. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 77c773a05b2..39045668160 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -87,6 +87,22 @@ BR2_PACKAGE_MALI_G52_ODROIDM1=y # project_odroid_retro_track_c_v3.md for the full gate 1/2 rationale. BR2_PACKAGE_BATOCERA_WAYLAND_LABWC=y +# libgtk3 defaults its Wayland GDK backend to on whenever BR2_PACKAGE_ +# WAYLAND + HAS_LIBEGL are both present (true here as of labwc above) - +# pcsx2 pulls in libgtk3 unconditionally (LIBGTK3, not the wayland +# suboption specifically). g29p1 exports the EGL platform functions GTK +# actually calls through (eglGetPlatformDisplay/eglInitialize/ +# eglCreatePlatformWindowSurface all present, confirmed via nm -D) but +# not wl_egl_window_create/resize/destroy - the separate libwayland-egl +# convenience wrapper some clients (GTK's GDK backend among them) build +# against instead of calling the platform functions directly. Nothing +# on this board provides those symbols (no working Mesa here, and the +# blob doesn't export them either), so libgtk3-wayland fails to link +# ("undefined reference to wl_egl_window_resize" etc.) - X11 stays on +# via BR2_PACKAGE_XORG7, so this only drops GTK's Wayland-specific GDK +# backend, not GTK itself. +# BR2_PACKAGE_LIBGTK3_WAYLAND is not set + # libcurl was previously built with brotli support because BR2_PACKAGE_BROTLI # got pulled in transitively via webkitgtk -> woff2 -> brotli, and webkitgtk # was only reachable through the GTK backend that Wayland/labwc provided. From 74309d9362e2270154cc7cb1a3fb9cae07269550 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Thu, 13 Aug 2026 11:56:20 +0900 Subject: [PATCH 017/110] mali-g52-odroidm1: stop clobbering wayland's own libwayland-egl.so Root cause of the wl_egl_window_* link failures (kodi, then libgtk3's Wayland backend) wasn't a genuine gap in this board's Wayland-EGL support - it was mali-g52-odroidm1.mk's own EGL/GLES soname-aliasing loop overwriting a real, working libwayland-egl.so.1 that buildroot's plain "wayland" package already builds (confirmed via nm -D: wl_egl_ window_create/destroy/resize/get_attached_size all present, pure malloc/free bookkeeping with zero GPU dependency - this symbol set belongs to wayland.org's own reference implementation, not the GPU driver, unlike EGL/GLES/GBM which genuinely do need to come from the blob). Dropping wayland-egl from the alias loop lets wayland's real file stand, so this reverts the libgtk3-wayland-backend workaround from the previous commit - it was solving a problem we caused ourselves. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 16 ---------------- .../mali-g52-odroidm1/mali-g52-odroidm1.mk | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 39045668160..77c773a05b2 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -87,22 +87,6 @@ BR2_PACKAGE_MALI_G52_ODROIDM1=y # project_odroid_retro_track_c_v3.md for the full gate 1/2 rationale. BR2_PACKAGE_BATOCERA_WAYLAND_LABWC=y -# libgtk3 defaults its Wayland GDK backend to on whenever BR2_PACKAGE_ -# WAYLAND + HAS_LIBEGL are both present (true here as of labwc above) - -# pcsx2 pulls in libgtk3 unconditionally (LIBGTK3, not the wayland -# suboption specifically). g29p1 exports the EGL platform functions GTK -# actually calls through (eglGetPlatformDisplay/eglInitialize/ -# eglCreatePlatformWindowSurface all present, confirmed via nm -D) but -# not wl_egl_window_create/resize/destroy - the separate libwayland-egl -# convenience wrapper some clients (GTK's GDK backend among them) build -# against instead of calling the platform functions directly. Nothing -# on this board provides those symbols (no working Mesa here, and the -# blob doesn't export them either), so libgtk3-wayland fails to link -# ("undefined reference to wl_egl_window_resize" etc.) - X11 stays on -# via BR2_PACKAGE_XORG7, so this only drops GTK's Wayland-specific GDK -# backend, not GTK itself. -# BR2_PACKAGE_LIBGTK3_WAYLAND is not set - # libcurl was previously built with brotli support because BR2_PACKAGE_BROTLI # got pulled in transitively via webkitgtk -> woff2 -> brotli, and webkitgtk # was only reachable through the GTK backend that Wayland/labwc provided. diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk index 2e0656345ff..79d763b204a 100644 --- a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk +++ b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk @@ -88,8 +88,23 @@ define MALI_G52_ODROIDM1_EXTRACT_CMDS # anything else) meant to be fixed up at runtime by libmali-hook / # mali-priority.sh: not something a static link at build time can # rely on. libmali.so.1.9.0 exports every symbol directly (EGL, - # GLESv1/2, GBM, wayland-egl - confirmed each with nm -D). - for lib in EGL:1 GLESv1_CM:1 GLESv2:2 MaliOpenCL:1 gbm:1 wayland-egl:1; do \ + # GLESv1/2, GBM - confirmed each with nm -D). + # + # wayland-egl is NOT in this list (was, up to track C): it doesn't + # belong to the GPU driver at all - wl_egl_window_* is pure struct + # bookkeeping (a malloc'd width/height/surface-pointer record the EGL + # platform code reads directly), not a real driver entry point, so + # it's the upstream wayland.org project's own reference impl that + # ships it - and buildroot's plain "wayland" package already builds + # and installs a real, working libwayland-egl.so.1 (confirmed via + # nm -D: wl_egl_window_create/destroy/resize/get_attached_size all + # present). g29p1 doesn't export any of these under either name + # (confirmed via nm -D on the raw blob) - aliasing to libmali.so.1.9.0 + # like the others silently overwrote wayland's real implementation + # with a broken one, only noticed once track C turned Wayland on and + # something that actually creates an EGL window surface (kodi, then + # libgtk3's Wayland GDK backend) tried to link against it. + for lib in EGL:1 GLESv1_CM:1 GLESv2:2 MaliOpenCL:1 gbm:1; do \ name=$${lib%%:*}; ver=$${lib##*:}; \ ln -sf libmali.so.1.9.0 $(@D)/usr/lib/aarch64-linux-gnu/lib$${name}.so.$${ver}; \ ln -sf lib$${name}.so.$${ver} $(@D)/usr/lib/aarch64-linux-gnu/lib$${name}.so; \ From 7265f1863becb9717e81457a111203eb8fb6e5c1 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Fri, 14 Aug 2026 23:08:26 +0900 Subject: [PATCH 018/110] sdl2: pass EGL_PLATFORM_WAYLAND_KHR for Wayland EGL display (Mali blob) Wayland_GLES_LoadLibrary() hardcoded platform=0, which skips the SDL_VIDEO_DRIVER_MALI dispatch batocera already added in SDL_egl.c (sdl2_add_video_mali_gles2.patch) and falls through to the legacy eglGetDisplay(native_display). The g29p1 proprietary blob does not support Wayland auto-detection on that legacy path and returns EGL_NO_DISPLAY, which surfaced as EmulationStation crash-looping with "Could not get EGL display" on every boot. Confirmed on real hardware: a raw ctypes EGL test using eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_KHR, ...) succeeds, while replicating the exact legacy eglGetDisplay(wl_display) call SDL was making fails with EGL_NO_DISPLAY. Passing EGL_PLATFORM_WAYLAND_KHR unconditionally (not gated behind SDL_VIDEO_DRIVER_MALI) is safe for Mesa too, since EGL_KHR_platform_wayland is a standard extension. Batocera's existing Mali patch only wired up kmsdrm (EGL_PLATFORM_GBM_MESA); nothing had previously combined the Mali blob with Wayland, so this gap was never hit before track C. --- .../sdl2_mali_wayland_platform_display.patch | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 board/batocera/patches/sdl2/sdl2_mali_wayland_platform_display.patch diff --git a/board/batocera/patches/sdl2/sdl2_mali_wayland_platform_display.patch b/board/batocera/patches/sdl2/sdl2_mali_wayland_platform_display.patch new file mode 100644 index 00000000000..5c0e13ef7e6 --- /dev/null +++ b/board/batocera/patches/sdl2/sdl2_mali_wayland_platform_display.patch @@ -0,0 +1,20 @@ +--- a/src/video/wayland/SDL_waylandopengles.c 2026-08-14 23:02:13.895506292 +0900 ++++ b/src/video/wayland/SDL_waylandopengles.c 2026-08-14 23:02:22.119634687 +0900 +@@ -40,7 +40,16 @@ + int ret; + SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + +- ret = SDL_EGL_LoadLibrary(_this, path, (NativeDisplayType) data->display, 0); ++ /* Proprietary Mali blobs (SDL_VIDEO_DRIVER_MALI, see the SDL_egl.c hook ++ * added by sdl2_add_video_mali_gles2.patch) only resolve a Wayland ++ * EGLDisplay via eglGetPlatformDisplayEXT/eglGetPlatformDisplay ++ * (EGL_KHR_platform_wayland). The legacy eglGetDisplay(wl_display*) ++ * auto-detection that Mesa supports is not implemented by the blob and ++ * returns EGL_NO_DISPLAY ("Could not get EGL display"). Passing ++ * platform=0 here (as upstream SDL2 does) skips that Mali dispatch ++ * entirely. EGL_KHR_platform_wayland is a standard extension Mesa ++ * supports too, so passing it unconditionally is safe there as well. */ ++ ret = SDL_EGL_LoadLibrary(_this, path, (NativeDisplayType) data->display, EGL_PLATFORM_WAYLAND_KHR); + + Wayland_PumpEvents(_this); + WAYLAND_wl_display_flush(data->display); From b93d99b42ac3b37243a3c55b19bd8ad2a156281a Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 09:15:28 +0900 Subject: [PATCH 019/110] configgen: bind RetroArch's exit_emulator to the same button as Start writeHotKeyConfig() only ever wrote input_enable_hotkey_btn (arming the hotkey modifier), never any joypad action bound to it - cleanControllerConfig even actively disable_all()s input_exit_emulator* every game launch via prefix match. Only the keyboard input_exit_emulator="escape" default actually quit a running libretro core; Hotkey+Start (batocera's own force-quit convention, see es_input.cfg's keyboard hotkey=select binding) did nothing on a gamepad. Confirmed live on ODROID-M1 (SHAKS S6b pad, FBNeo Korean core/Night Slashers): Select+Start did not exit until input_exit_emulator_btn was set to Start's button id. This is shared configgen code, not board- or GPU-track-specific - the gap should be identical on A/B, just never exercised there. --- .../configgen/generators/libretro/libretroControllers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroControllers.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroControllers.py index a30b3467a84..e9971babcb3 100755 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroControllers.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/libretro/libretroControllers.py @@ -89,6 +89,15 @@ def cleanControllerConfig(retroconfig: UnixSettings, controllers: Controllers, / def writeHotKeyConfig(retroconfig: UnixSettings, controllers: Controllers, /) -> None: if controllers and 'hotkey' in controllers[0].inputs and controllers[0].inputs['hotkey'].type == 'button': retroconfig.save('input_enable_hotkey_btn', controllers[0].inputs['hotkey'].id) + # Without this, input_enable_hotkey_btn alone arms the hotkey modifier + # but no joypad action is bound to it, so Hotkey+Start (the batocera + # keyboard convention for force-quitting a game, see es_input.cfg's + # keyboard hotkey=select binding) does nothing on a gamepad - only the + # keyboard input_exit_emulator="escape" above ever quits a running + # libretro core. Confirmed live on hardware (ODROID-M1, SHAKS S6b pad): + # Hotkey+Start did not exit FBNeo until this was added. + if controllers and 'start' in controllers[0].inputs and controllers[0].inputs['start'].type == 'button': + retroconfig.save('input_exit_emulator_btn', controllers[0].inputs['start'].id) # Write a configuration for a specified controller def writeControllerConfig( From b24dccc642264fc39a9aba74ee7d30af4c2ca3d1 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 09:44:57 +0900 Subject: [PATCH 020/110] odroidm1: enable BATOCERA_VULKAN for track C - needed for ES UI options too This board's Vulkan driver comes from the g29p1 blob (hand-written mali.json ICD), not a Kconfig-selectable Mesa driver, so it was deliberately left off - but that missed that the option also registers PPSSPP's gfxbackend.ppsspp.emulator.yml (the Vulkan choice in ES's own PSP graphics-API menu never appeared without it) and selects VKQUAKE/VKQUAKE2/VKQUAKE3/QT6BASE_VULKAN. Confirmed safe: every Mesa Vulkan driver select this option pulls in is itself gated behind other targets (X86/RPi/PANFROST_MESA3D/ FREEDRENO_MESA3D) that don't match rk3568/bsp-odroidm1, so enabling it here only adds the generic vulkan-tools/spirv-tools/shaderc packages plus the Vulkan-native ports/UI options above - no conflicting driver. --- configs/batocera-rk3568-odroidm1.board | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 77c773a05b2..f03739b3a89 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -135,14 +135,28 @@ BR2_PACKAGE_LIBRETRO_FBNEO_KOREAN=y BR2_PACKAGE_LIBRETRO_MELONDS_DS=y # Track C (g29p1 Vulkan blob + Wayland, see project memory -# project_odroid_retro_track_c_v3.md) systems/cores. No BATOCERA_VULKAN/ -# MESA3D_VULKAN_DRIVER_* line here - unlike B's PanVK, this board's Vulkan -# comes from the g29p1 blob itself (mali.json ICD written by hand once the -# blob is swapped in, not a buildroot-selectable Mesa driver) - so nothing -# to select at the Kconfig level for that part, it's a runtime/package -# addition to mali-g52-odroidm1.mk once gate 1/2 pass. These system/core -# enables are GPU-driver-independent and were already confirmed to build -# on B (cherry-picked selects only, not the PanVK-specific lines): +# project_odroid_retro_track_c_v3.md) systems/cores. +# +# BR2_PACKAGE_BATOCERA_VULKAN is needed here despite this board's Vulkan +# coming from the g29p1 blob itself (mali.json ICD written by hand once +# the blob is swapped in), not a buildroot-selectable Mesa driver - +# initially left off on the (correct as far as it went) reasoning that +# there was "nothing to select at the Kconfig level" for the driver, but +# that option gates more than just Mesa Vulkan driver selection: it's +# also what registers PPSSPP's gfxbackend.ppsspp.emulator.yml (the +# Graphics API/Vulkan choice in ES's own PSP options menu never appeared +# without it - confirmed live, ES only showed OpenGL) and selects +# VKQUAKE/VKQUAKE2/VKQUAKE3/QT6BASE_VULKAN. Confirmed safe on this board: +# every BR2_PACKAGE_MESA3D_VULKAN_DRIVER_* select it pulls in is itself +# gated behind other targets (X86/RPi/PANFROST_MESA3D/FREEDRENO_MESA3D) +# that don't match rk3568/bsp-odroidm1, so enabling this adds only the +# generic vulkan-tools/spirv-tools/shaderc packages plus the above +# Vulkan-native ports/UI options, no conflicting Mesa driver. +BR2_PACKAGE_BATOCERA_VULKAN=y +# +# These system/core enables are GPU-driver-independent and were already +# confirmed to build on B (cherry-picked selects only, not the +# PanVK-specific lines): # # PCSX2 (PS2) - never tested on this board before B. Needs BR2_PACKAGE_ # HAS_LIBGL fixed above (see vkquake2/pcsx2 desktop-GL commit) to avoid From a0ad43e1648c243788ad16b76bed934b911f397b Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 10:31:47 +0900 Subject: [PATCH 021/110] gpu: add vulkan-wsi-layer package - bridges g29p1 blob's KHR_display-only Vulkan to Wayland surfaces Confirmed live via vulkaninfo's Instance Extensions list: the g29p1 blob's Vulkan ICD only implements VK_KHR_display (bare DRM, no compositor) among windowing-system surface extensions - no VK_KHR_wayland_surface at all. Without this, every Vulkan app's own surface/device creation for a Wayland window fails outright even though vulkaninfo enumerates the GPU fine (confirmed live: PPSSPP's SDL2 window creation succeeded once SDL2 itself got Vulkan support enabled via the sdl2.mk buildroot-submodule fix, but PPSSPP's own Vulkan backend init then failed and silently fell back to GLES). New package mirrors ROCKNIX's own fix for the same blob/board family (RK3566/g29p1, projects/ROCKNIX/packages/graphics/vulkan-wsi-layer/ package.mk) - ARM's open-source vulkan-wsi-layer implements VK_KHR_wayland_surface as a Vulkan layer on top of the bare ICD. Deviates from ROCKNIX's recipe in two places, both confirmed live: - WSIALLOC_MEMORY_HEAP_NAME=system, not their "linux,cma" - this BSP 6.1 kernel's /dev/dma_heap/ only has reserved/system/system-uncached, no heap named "linux,cma" (different DMA-heap exporter/naming than whatever ROCKNIX's own kernel registers under that name). - Layer .so and .json manifest installed to the SAME directory (/usr/share/vulkan/implicit_layer.d/), not split across /usr/lib + implicit_layer.d like ROCKNIX's package.mk - the manifest's "library_path" is the relative "./libVkLayer_window_system_integration.so", which the Khronos loader resolves against the directory containing the *manifest*, not against /usr/lib. Left unverified whether ROCKNIX's split actually works for them; co-locating is unambiguously correct per the loader's documented resolution rule. Requires BR2_PACKAGE_BATOCERA_VULKAN=y - previously left off (see the .board comment above this select) on the correct-as-far-as-it-went reasoning that this board has no Kconfig-selectable Mesa Vulkan driver, but that option also gates PPSSPP's gfxbackend.ppsspp.emulator.yml (the Vulkan choice in ES's own PSP graphics-API menu never appeared without it) and, as of this commit, this layer. Status: builds and installs cleanly, loads and gets dispatched to (vkCreateWaylandSurfaceKHR is called - confirmed via vulkaninfo, this extension wasn't even recognized before the layer existed). Currently blocked on a live, unresolved failure: the call returns VK_ERROR_OUT_OF_HOST_MEMORY while enumerating labwc's zwp_linux_dmabuf_v1 format/modifier list (wsi/wayland/surface.cpp's get_supported_formats_and_modifiers(), custom_allocator-backed util::vector filling from the dmabuf format/modifier event listener - this board's Mali AFBC modifier list is very large, hundreds of format+modifier pairs per `wayland-info`, prime suspect but unconfirmed without a debugger). VULKAN_WSI_DEBUG_LEVEL=5 produces no extra output; MangoHud layer interaction ruled out (DISABLE_MANGOHUD=1, same failure). See project Joplin note for the full investigation writeup. --- Config.in | 1 + configs/batocera-rk3568-odroidm1.board | 13 ++++ .../gpu/vulkan/vulkan-wsi-layer/Config.in | 15 ++++ .../vulkan-wsi-layer/vulkan-wsi-layer.mk | 75 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 package/batocera/gpu/vulkan/vulkan-wsi-layer/Config.in create mode 100644 package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk diff --git a/Config.in b/Config.in index de2bd0d29cf..085af43d6d8 100644 --- a/Config.in +++ b/Config.in @@ -118,6 +118,7 @@ menu "System" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/vulkan/spirv-cross/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/vulkan/vulkan-utility-libraries/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/vulkan/vulkan-validationlayers/Config.in" + source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/vulkan/vulkan-wsi-layer/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/gtk-layer-shell/Config.in" endmenu diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index f03739b3a89..dc6b4821185 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -154,6 +154,19 @@ BR2_PACKAGE_LIBRETRO_MELONDS_DS=y # Vulkan-native ports/UI options, no conflicting Mesa driver. BR2_PACKAGE_BATOCERA_VULKAN=y # +# The g29p1 blob's Vulkan ICD only implements VK_KHR_display (bare DRM, no +# compositor) among windowing-system surface extensions - confirmed live +# via vulkaninfo's Instance Extensions list, no VK_KHR_wayland_surface at +# all. Without this layer, every Vulkan app's own surface/device creation +# for a Wayland window fails outright (confirmed live: PPSSPP's SDL2 +# window creation succeeded once SDL2 itself got Vulkan support enabled, +# but PPSSPP's own Vulkan backend init then failed and silently fell back +# to another backend) even though vulkaninfo enumerates the GPU fine. +# ROCKNIX carries the exact same fix for the same blob/board family +# (RK3566/g29p1) - see package/batocera/gpu/vulkan/vulkan-wsi-layer/ for +# the full writeup and version pin. +BR2_PACKAGE_VULKAN_WSI_LAYER=y +# # These system/core enables are GPU-driver-independent and were already # confirmed to build on B (cherry-picked selects only, not the # PanVK-specific lines): diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/Config.in b/package/batocera/gpu/vulkan/vulkan-wsi-layer/Config.in new file mode 100644 index 00000000000..5f290acd14b --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/Config.in @@ -0,0 +1,15 @@ +config BR2_PACKAGE_VULKAN_WSI_LAYER + bool "vulkan-wsi-layer" + depends on BR2_PACKAGE_WAYLAND + select BR2_PACKAGE_VULKAN_HEADERS + select BR2_PACKAGE_VULKAN_LOADER + select BR2_PACKAGE_LIBDRM + select BR2_PACKAGE_WAYLAND_PROTOCOLS + help + ARM's open-source Vulkan layer implementing VK_KHR_wayland_surface + (window system integration) on top of an ICD that only provides + bare VK_KHR_display - needed for the Mali g29p1 proprietary blob, + whose Vulkan ICD does not implement any windowing-system surface + extension itself. + + https://gitlab.freedesktop.org/mesa/vulkan-wsi-layer diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk b/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk new file mode 100644 index 00000000000..8f598d1f882 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk @@ -0,0 +1,75 @@ +################################################################################ +# +# vulkan-wsi-layer +# +################################################################################ +# ARM's open-source Vulkan layer implementing VK_KHR_wayland_surface (and +# other WSI backends) on top of an ICD that doesn't provide its own - +# confirmed live that the g29p1 blob's Vulkan ICD only exports VK_KHR_display +# (bare DRM, no compositor) among windowing-system extensions: no +# VK_KHR_wayland_surface, so no Vulkan app can create a surface for a +# Wayland window without this layer sitting in front of it (SDL2/PPSSPP's +# own Vulkan surface/device creation otherwise fails outright even though +# vulkaninfo enumerates the GPU fine - confirmed live, this is a real ICD +# capability gap, not a downstream packaging/config bug). Same technique +# ROCKNIX uses for the same blob/board family (RK3566/g29p1), version pin +# and build flags below mirror their package.mk 1:1 (projects/ROCKNIX/ +# packages/graphics/vulkan-wsi-layer/package.mk). +VULKAN_WSI_LAYER_VERSION = 8f077c5c862e5259841d524de8280b8c2429990a +VULKAN_WSI_LAYER_SITE = https://gitlab.freedesktop.org/mesa/vulkan-wsi-layer.git +VULKAN_WSI_LAYER_SITE_METHOD = git +VULKAN_WSI_LAYER_LICENSE = MIT +VULKAN_WSI_LAYER_LICENSE_FILES = LICENSE + +VULKAN_WSI_LAYER_DEPENDENCIES = libdrm wayland-protocols vulkan-loader vulkan-headers linux + +# BUILD_WSI_WAYLAND/DISPLAY require an external DMA allocator - dma_buf_heaps +# is what ROCKNIX uses for every non-RK3588 (BSP-kernel) device, matching +# this board (BSP 6.1, not mainline). +# +# WSIALLOC_MEMORY_HEAP_NAME: ROCKNIX's non-RK3588 value is "linux,cma", but +# that heap doesn't exist on this BSP 6.1 kernel - confirmed live, this +# board's /dev/dma_heap/ only has "reserved", "system", "system-uncached" +# (a different DMA-heap exporter/naming scheme than whatever ROCKNIX's own +# kernel registers under "linux,cma"). "system" is the generic CMA-backed +# general-purpose heap here. +VULKAN_WSI_LAYER_DMA_HEAP = system +# +# KERNEL_HEADER_DIR needs the *configured* kernel source tree (drm_utils +# includes kernel DRM/fourcc headers directly, not the installed userspace +# uapi headers) - $(LINUX_DIR) is buildroot's own extracted+configured +# linux package dir, already built by the time this package builds since +# it's declared as a dependency above. +VULKAN_WSI_LAYER_CONF_OPTS = \ + -DVULKAN_CXX_INCLUDE=$(STAGING_DIR)/usr/include \ + -DBUILD_WSI_HEADLESS=OFF \ + -DBUILD_WSI_WAYLAND=ON \ + -DSELECT_EXTERNAL_ALLOCATOR=dma_buf_heaps \ + -DWSIALLOC_MEMORY_HEAP_NAME=$(VULKAN_WSI_LAYER_DMA_HEAP) \ + -DENABLE_WAYLAND_FIFO_PRESENTATION_THREAD=ON \ + -DKERNEL_HEADER_DIR=$(LINUX_DIR)/arch/arm64/include \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + +# Installed as an implicit layer (auto-activates for every Vulkan app, no +# VK_INSTANCE_LAYERS needed) - same mechanism this board's MangoHud Vulkan +# overlay already uses (/usr/share/vulkan/implicit_layer.d/). +# +# Both files go in the SAME directory: the upstream manifest's +# "library_path" is the relative "./libVkLayer_window_system_integration.so" +# (confirmed in layer/VkLayer_window_system_integration.json), and the +# Khronos loader resolves a relative library_path against the directory +# containing the *manifest*, not against /usr/lib or any library search +# path. ROCKNIX's own package.mk installs the .so to /usr/lib/ and the +# .json to implicit_layer.d/ separately - do not copy that split, it would +# leave the relative path unresolvable here (untested whether that split +# actually works for them; not worth the risk when co-locating is simpler +# and unambiguously correct per the loader's own documented resolution +# rule). +define VULKAN_WSI_LAYER_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0755 $(@D)/libVkLayer_window_system_integration.so \ + $(TARGET_DIR)/usr/share/vulkan/implicit_layer.d/libVkLayer_window_system_integration.so + $(INSTALL) -D -m 0644 $(@D)/VkLayer_window_system_integration.json \ + $(TARGET_DIR)/usr/share/vulkan/implicit_layer.d/VkLayer_window_system_integration.json +endef + +$(eval $(cmake-package)) From 38d396bdd2ba4926d2d6e542d2955280709918aa Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 10:32:06 +0900 Subject: [PATCH 022/110] buildroot: bump submodule for the SDL2 video-vulkan gating fix (uncommitted upstream) Points at a detached-HEAD commit (d851f79697) on top of the existing buildroot fork pin (95d26b3e3f) - not yet pushed anywhere, since this worktree's buildroot submodule only has the batocera-linux/buildroot upstream remote configured, no personal fork. The fix itself: package/ sdl2/sdl2.mk no longer requires a Mesa Vulkan driver to enable --enable-video-vulkan (see that commit's own message). Needs a real fork remote added and pushed before this is reproducible from a fresh clone. --- buildroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildroot b/buildroot index 95d26b3e3ff..d851f796972 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit 95d26b3e3ff0a88446aaed4ab2a27c9504ae838c +Subproject commit d851f7969721cb3e2df923df6bd9075d3898e175 From f088699e494f7aefd22b6111c81e3a6c2953dc34 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 11:18:57 +0900 Subject: [PATCH 023/110] vulkan-wsi-layer: fix real root cause of Wayland surface/swapchain failure Corrects a0ad43e's misdiagnosis. That commit's Status note blamed VK_ERROR_OUT_OF_HOST_MEMORY on the dmabuf modifier-list vector (custom_allocator OOM in get_supported_formats_and_modifiers) - that's wrong. The real failure happens earlier and never reaches that code: wsi/wayland/surface.cpp's surface::init() hard-requires the zwp_linux_explicit_synchronization_v1 Wayland global, which wlroots (labwc's compositor) has never implemented - it went straight to the newer linux-drm-syncobj-v1 model instead. Every failure branch inside surface::init() (missing dmabuf/explicit_sync/presentation interface, roundtrip failure, real OOM) gets collapsed into the same generic VK_ERROR_OUT_OF_HOST_MEMORY by CreateWaylandSurfaceKHR's caller (surface_properties.cpp), which is what made the error code so misleading. Confirmed live: `wayland-info | grep -i explicit` on this device returns nothing, and (once logging was actually working, see below) WSI_LOG_ERROR at the exact explicit_sync check fired every time. Also root-caused why VULKAN_WSI_DEBUG_LEVEL was a silent no-op the entire time this was being investigated: util/log.hpp gates wsi_log_enable behind `#ifdef NDEBUG`, and buildroot's cmake-package infra defaults new packages to CMAKE_BUILD_TYPE=Release (-DNDEBUG) - so every WSI_LOG_ERROR call across the whole layer was compiled out to nothing, regardless of the env var. No gdb or extra tooling was needed once this was found - just build Debug. Fix: apply ROCKNIX's own sync.patch for this exact pinned layer commit (8f077c5, matches VULKAN_WSI_LAYER_VERSION here) - authored by Ivan Podogov (ginkage), who wrote the layer's Wayland backend upstream and ships this same patch to run it on wlroots (sway) compositors. Makes explicit sync optional with an implicit dma-buf-fence fallback (CPU-side wait_payload when no acquire-fence interface is available), forces FIFO present mode (implicit sync isn't safe with MAILBOX/IMMEDIATE), remaps ARGB8888/ABGR8888 to the opaque XRGB8888/XBGR8888 fourccs compositors advertise more reliably, adds VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, and disables the ABGR2101010 format table entry. Saved verbatim as 0001-fall-back-to-implicit-sync.patch for buildroot's automatic per-package patch application; explicitly NOT applying ROCKNIX's sibling vklayer-driver-path.patch (bare-soname library_path) since that would break this package's current co-located implicit_layer.d install layout. Also, independently of the above (found live, not covered by the ROCKNIX patch): the manifest's relative "./libVkLayer_..." library_path failed to dlopen for at least one real launch context (PPSSPP via SSH, different CWD) even though it worked for vulkaninfo - switched mali.json's sibling manifest... n/a here, but noting for the record: this package's own install step already co-locates the .so and .json, matching what turned out to be necessary. DMA heap: switched WSIALLOC_MEMORY_HEAP_NAME from "system" to "system-uncached", per ginkage's own README recommendation for this allocator backend - "system" is a cached page-allocator heap (not CMA/contiguous, correcting an earlier version of this comment), and a cached heap risks coherency artefacts on buffers shared between GPU and compositor. CMAKE_BUILD_TYPE=Debug added temporarily for bring-up so WSI_LOG_ERROR/WARNING actually print - remove once the Vulkan surface/swapchain path is confirmed stable end-to-end (revert to buildroot's Release default). Verified live end-to-end: vkCreateWaylandSurfaceKHR and vkCreateSwapchainKHR both succeed (previously OUT_OF_HOST_MEMORY / INITIALIZATION_FAILED), PPSSPP with GraphicsBackend=VULKAN runs a 20s+ session with no fallback to GL, no dmesg GPU faults, and multiple real Vulkan pipelines compiled+cached (ShaderManagerVulkan/ PipelineManagerVulkan save messages). This is Track C's first actual Vulkan-rendered frame. Credit: root-caused independently in this session via direct source reading (explicit_sync requirement + NDEBUG logging gate) and cross-checked against an equivalent independent analysis from another session, which additionally identified ROCKNIX's existing sync.patch as the vetted fix and system-uncached as the correct heap - both adopted here instead of a hand-rolled equivalent. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../0001-fall-back-to-implicit-sync.patch | 189 ++++++++++++++++++ .../vulkan-wsi-layer/vulkan-wsi-layer.mk | 19 +- 2 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch new file mode 100644 index 00000000000..9c81ea43ad9 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch @@ -0,0 +1,189 @@ +From 8dfe4f4a9bde2968b3edb22bbdf4f8ddc102fafd Mon Sep 17 00:00:00 2001 +From: Ivan Podogov +Date: Mon, 11 Nov 2024 09:44:12 +0000 +Subject: [PATCH] Fall back to implicit sync if necessary + +--- + README.md | 7 ++++--- + layer/swapchain_api.cpp | 4 +++- + util/drm/format_table.c | 2 +- + wsi/wayland/surface.cpp | 25 ++++++++++------------ + wsi/wayland/surface_properties.cpp | 4 ++-- + wsi/wayland/swapchain.cpp | 33 ++++++++++++++++++++---------- + 6 files changed, 48 insertions(+), 42 deletions(-) + +diff --git a/README.md b/README.md +index 55afcb9..481a0c6 100644 +--- a/README.md ++++ b/README.md +@@ -102,11 +102,12 @@ the `KERNEL_HEADER_DIR` option must be defined as the directory that includes th + source. + + ``` +-cmake . -DVULKAN_CXX_INCLUDE="path/to/vulkan-header" \ ++cmake . -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_WSI_HEADLESS=0 \ + -DBUILD_WSI_WAYLAND=1 \ +- -DSELECT_EXTERNAL_ALLOCATOR=ion \ +- -DKERNEL_HEADER_DIR="path/to/linux-kernel-headers" ++ -DSELECT_EXTERNAL_ALLOCATOR=dma_buf_heaps \ ++ -DWSIALLOC_MEMORY_HEAP_NAME=system-uncached \ ++ -DENABLE_WAYLAND_FIFO_PRESENTATION_THREAD=1 + ``` + + In the command line above, `-DBUILD_WSI_HEADLESS=0` is used to disable support +diff --git a/layer/swapchain_api.cpp b/layer/swapchain_api.cpp +index e027c29..b9d11e3 100644 +--- a/layer/swapchain_api.cpp ++++ b/layer/swapchain_api.cpp +@@ -65,7 +65,9 @@ wsi_layer_vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR * + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + +- TRY_LOG(sc->init(device, pSwapchainCreateInfo), "Failed to initialise swapchain"); ++ VkSwapchainCreateInfoKHR my_create_info = *pSwapchainCreateInfo; ++ my_create_info.presentMode = VK_PRESENT_MODE_FIFO_KHR; ++ TRY_LOG(sc->init(device, &my_create_info), "Failed to initialise swapchain"); + + TRY_LOG(device_data.add_layer_swapchain(reinterpret_cast(sc.get())), + "Failed to associate swapchain with the layer"); +diff --git a/util/drm/format_table.c b/util/drm/format_table.c +index d0d93e8..b1e1858 100644 +--- a/util/drm/format_table.c ++++ b/util/drm/format_table.c +@@ -57,7 +57,7 @@ const fmt_spec fourcc_format_table[] = { + { DRM_FORMAT_RGBA8888, 1, { 32, 0, 0, 0 }, VK_FORMAT_UNDEFINED }, + { DRM_FORMAT_BGRA8888, 1, { 32, 0, 0, 0 }, VK_FORMAT_UNDEFINED }, + +- { DRM_FORMAT_ABGR2101010, 1, { 32, 0, 0, 0 }, VK_FORMAT_A2B10G10R10_UNORM_PACK32 }, ++ //{ DRM_FORMAT_ABGR2101010, 1, { 32, 0, 0, 0 }, VK_FORMAT_A2B10G10R10_UNORM_PACK32 }, + }; + + const fmt_spec srgb_fourcc_format_table[] = { +diff --git a/wsi/wayland/surface.cpp b/wsi/wayland/surface.cpp +index 38ff3b3..ff8fbbe 100644 +--- a/wsi/wayland/surface.cpp ++++ b/wsi/wayland/surface.cpp +@@ -233,27 +233,24 @@ bool surface::init() + return false; + } + +- if (explicit_sync_interface.get() == nullptr) +- { +- WSI_LOG_ERROR("Failed to obtain zwp_linux_explicit_synchronization_v1 interface."); +- return false; +- } +- + if (presentation_time_interface.get() == nullptr) + { + WSI_LOG_ERROR("Failed to obtain wp_presentation interface."); +- return false; ++ //return false; + } + +- auto surface_sync_obj = +- zwp_linux_explicit_synchronization_v1_get_synchronization(explicit_sync_interface.get(), wayland_surface); +- if (surface_sync_obj == nullptr) ++ if (explicit_sync_interface.get() != nullptr) + { +- WSI_LOG_ERROR("Failed to retrieve surface synchronization interface"); +- return false; +- } ++ auto surface_sync_obj = ++ zwp_linux_explicit_synchronization_v1_get_synchronization(explicit_sync_interface.get(), wayland_surface); ++ if (surface_sync_obj == nullptr) ++ { ++ WSI_LOG_ERROR("Failed to retrieve surface synchronization interface"); ++ return false; ++ } + +- surface_sync_interface.reset(surface_sync_obj); ++ surface_sync_interface.reset(surface_sync_obj); ++ } + + VkResult vk_res = get_supported_formats_and_modifiers(wayland_display, surface_queue.get(), dmabuf_interface.get(), + supported_formats); +diff --git a/wsi/wayland/surface_properties.cpp b/wsi/wayland/surface_properties.cpp +index 31ddc68..96b7729 100644 +--- a/wsi/wayland/surface_properties.cpp ++++ b/wsi/wayland/surface_properties.cpp +@@ -89,7 +89,7 @@ VkResult surface_properties::get_surface_capabilities(VkPhysicalDevice physical_ + + /* Composite alpha */ + pSurfaceCapabilities->supportedCompositeAlpha = static_cast( +- VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR | VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR); ++ VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR | VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR | VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR); + return VK_SUCCESS; + } + +@@ -338,7 +338,7 @@ static bool check_wl_protocols(struct wl_display *display) + return false; + } + +- return (supported.dmabuf && supported.explicit_sync); ++ return (supported.dmabuf /* && supported.explicit_sync */); + } + + VWL_VKAPI_CALL(VkBool32) +diff --git a/wsi/wayland/swapchain.cpp b/wsi/wayland/swapchain.cpp +index 91cb23f..254fdbd 100644 +--- a/wsi/wayland/swapchain.cpp ++++ b/wsi/wayland/swapchain.cpp +@@ -436,9 +436,11 @@ VkResult swapchain::create_wl_buffer(const VkImageCreateInfo &image_create_info, + image_data->external_mem.get_strides()[plane], modifier_hi, modifier_low); + } + +- const auto fourcc = util::drm::vk_to_drm_format(image_create_info.format); ++ auto fourcc = util::drm::vk_to_drm_format(image_create_info.format); + assert(image_create_info.extent.width <= INT32_MAX); + assert(image_create_info.extent.height <= INT32_MAX); ++ if (fourcc == DRM_FORMAT_ARGB8888) fourcc = DRM_FORMAT_XRGB8888; ++ if (fourcc == DRM_FORMAT_ABGR8888) fourcc = DRM_FORMAT_XBGR8888; + image_data->buffer = zwp_linux_buffer_params_v1_create_immed(params, image_create_info.extent.width, + image_create_info.extent.height, fourcc, 0); + zwp_linux_buffer_params_v1_destroy(params); +@@ -545,16 +547,19 @@ void swapchain::present_image(const pending_present_request &pending_present) + + wl_surface_attach(m_surface, image_data->buffer, 0, 0); + +- auto present_sync_fd = image_data->present_fence.export_sync_fd(); +- if (!present_sync_fd.has_value()) ++ if (m_wsi_surface->get_surface_sync_interface() != nullptr) + { +- WSI_LOG_ERROR("Failed to export present fence."); +- set_error_state(VK_ERROR_SURFACE_LOST_KHR); +- } +- else if (present_sync_fd->is_valid()) +- { +- zwp_linux_surface_synchronization_v1_set_acquire_fence(m_wsi_surface->get_surface_sync_interface(), +- present_sync_fd->get()); ++ auto present_sync_fd = image_data->present_fence.export_sync_fd(); ++ if (!present_sync_fd.has_value()) ++ { ++ WSI_LOG_ERROR("Failed to export present fence."); ++ set_error_state(VK_ERROR_SURFACE_LOST_KHR); ++ } ++ else if (present_sync_fd->is_valid()) ++ { ++ zwp_linux_surface_synchronization_v1_set_acquire_fence(m_wsi_surface->get_surface_sync_interface(), ++ present_sync_fd->get()); ++ } + } + + /* TODO: work out damage */ +@@ -676,8 +681,14 @@ VkResult swapchain::image_set_present_payload(swapchain_image &image, VkQueue qu + return image_data->present_fence.set_payload(queue, semaphores, submission_pnext); + } + +-VkResult swapchain::image_wait_present(swapchain_image &, uint64_t) ++VkResult swapchain::image_wait_present(swapchain_image &image, uint64_t timeout) + { ++ if (m_wsi_surface->get_surface_sync_interface() == nullptr) ++ { ++ auto data = reinterpret_cast(image.data); ++ return data->present_fence.wait_payload(timeout); ++ } ++ + /* With explicit sync in use there is no need to wait for the present sync before submiting the image to the + * compositor. */ + return VK_SUCCESS; diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk b/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk index 8f598d1f882..6ee4a9b5dc2 100644 --- a/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk @@ -31,16 +31,29 @@ VULKAN_WSI_LAYER_DEPENDENCIES = libdrm wayland-protocols vulkan-loader vulkan-he # that heap doesn't exist on this BSP 6.1 kernel - confirmed live, this # board's /dev/dma_heap/ only has "reserved", "system", "system-uncached" # (a different DMA-heap exporter/naming scheme than whatever ROCKNIX's own -# kernel registers under "linux,cma"). "system" is the generic CMA-backed -# general-purpose heap here. -VULKAN_WSI_LAYER_DMA_HEAP = system +# kernel registers under "linux,cma"). Using "system-uncached" per +# ginkage's (ROCKNIX vulkan-wsi-layer sync.patch author) own README +# recommendation - "system" is a cached page-allocator heap (NOT +# CMA/contiguous, despite what an earlier version of this comment said), +# and a cached heap risks coherency artefacts (tearing/corruption) on +# buffers shared between the GPU and the compositor without a cache flush +# in between. +VULKAN_WSI_LAYER_DMA_HEAP = system-uncached # # KERNEL_HEADER_DIR needs the *configured* kernel source tree (drm_utils # includes kernel DRM/fourcc headers directly, not the installed userspace # uapi headers) - $(LINUX_DIR) is buildroot's own extracted+configured # linux package dir, already built by the time this package builds since # it's declared as a dependency above. +# TEMPORARY for bring-up: util/log.cpp's WSI_LOG_ERROR/WARNING calls are +# compiled out entirely under NDEBUG (see util/log.hpp's wsi_log_enable), +# which is why VULKAN_WSI_DEBUG_LEVEL was silently a no-op throughout the +# initial investigation - buildroot's cmake-package defaults to +# CMAKE_BUILD_TYPE=Release (-DNDEBUG). Force Debug here until the Vulkan +# surface/swapchain path is confirmed stable end-to-end, then remove this +# line (revert to buildroot's default Release) in a follow-up commit. VULKAN_WSI_LAYER_CONF_OPTS = \ + -DCMAKE_BUILD_TYPE=Debug \ -DVULKAN_CXX_INCLUDE=$(STAGING_DIR)/usr/include \ -DBUILD_WSI_HEADLESS=OFF \ -DBUILD_WSI_WAYLAND=ON \ From c736fc6e7c06e30d5beab3dba4c079c08d397a1f Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 12:25:01 +0900 Subject: [PATCH 024/110] vulkan-wsi-layer: fix vkCreateDevice failures for RetroArch/libretro Vulkan cores This Mali blob ICD rejects vkCreateDevice outright with VK_ERROR_INITIALIZATION_FAILED (rather than tolerating it, as more lenient drivers do) for two spec violations the layer could itself introduce or pass through unmodified. Found by tracing why libretro ppsspp_libretro.so (RetroArch) crashed with SIGABRT under Vulkan even though standalone PPSSPP (SDL2) already worked cleanly after the sync.patch fix - the device-creation path is entirely different between the two (RetroArch's own Vulkan HW render context negotiation vs. PPSSPP's own SDL2-based Vulkan init), so this is a genuinely separate bug from the explicit_sync one, discovered only once RetroArch's own VkResult (-3, VK_ERROR_INITIALIZATION_FAILED) was logged directly, since the core itself swallows the failure silently and proceeds to call vkGetDeviceQueue on the resulting invalid device handle, which is what the Khronos loader's validation actually catches and aborts on. 1. add_device_extensions_required_by_layer() could append an extension the application already requested itself - confirmed live: RetroArch's negotiation already requests VK_KHR_external_fence/_fd, which this function's own "optional extensions independent of winsys" list and its per-platform required-extensions list both also add unconditionally, producing a genuine duplicate extension name in ppEnabledExtensionNames (forbidden by VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-01387). Fixed by skipping anything extensions_to_enable already contains in both places. 2. RetroArch's own VkDeviceCreateInfo sets a legacy non-NULL pEnabledFeatures *and* chains a VkPhysicalDeviceFeatures2 in pNext at the same time (forbidden by VUID-VkDeviceCreateInfo-pNext-00373). The layer forwards the app's struct essentially unmodified, so this passed straight through. Fixed in create_device() by clearing pEnabledFeatures whenever a VkPhysicalDeviceFeatures2 is also present in pNext - the pNext version is a strict superset of what the legacy struct expresses, so dropping the redundant pointer is always safe. Saved as 0002-fix-device-create-info-spec-violations.patch (verified applying cleanly with `patch -p1 --dry-run` against a pristine checkout of the pinned commit 8f077c5, independent of 0001's hunks - touches different files). Verified live end-to-end: libretro ppsspp_libretro.so with video_driver=vulkan went from immediate SIGABRT to a full clean run - device created, Vulkan surface + 3-image swapchain created, FIFO present mode selected, no dmesg GPU faults, 10s+ sustained. This is a generic fix in the layer's own device-creation path, independent of windowing system, so it should also benefit any other libretro core using RetroArch's Vulkan HW render negotiation (parallel_n64/ParaLLEl-RDP, MAME with Vulkan support, etc.), not just PPSSPP. Note: standalone Dolphin (dolphin-emu, Qt6) remains separately broken under Track C - it fails even earlier, before any Vulkan/WSI call is ever reached, most likely in Qt's own native wl_surface handle acquisition under labwc (`-platform wayland` gets a real window on screen but "Failed to initialize video backend!"; `QT_QPA_PLATFORM` env var is inexplicably ignored in favour of `-platform` CLI arg, a still-unexplained Qt6 quirk on this build worth revisiting). Not attempted further this session - out of scope of this patch, which is specifically about the vkCreateDevice failures above. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- ...x-device-create-info-spec-violations.patch | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 package/batocera/gpu/vulkan/vulkan-wsi-layer/0002-fix-device-create-info-spec-violations.patch diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/0002-fix-device-create-info-spec-violations.patch b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0002-fix-device-create-info-spec-violations.patch new file mode 100644 index 00000000000..0d6e5b29829 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0002-fix-device-create-info-spec-violations.patch @@ -0,0 +1,114 @@ +From: Claude (odroid-m1-g29p1 session) +Subject: [PATCH] Fix vkCreateDevice failures against strict ICDs (duplicate + extensions, pEnabledFeatures/pNext conflict) + +Root-caused live against the ODROID-M1 g29p1 Mali blob ICD, which rejects +vkCreateDevice outright with VK_ERROR_INITIALIZATION_FAILED (rather than +tolerating it, as some more lenient drivers do) in two situations this +layer can itself introduce or pass through unmodified: + +1. add_device_extensions_required_by_layer() could append an extension the + application had already requested itself (e.g. RetroArch's own Vulkan + HW render context negotiation - used by libretro cores such as + ppsspp_libretro - already requests VK_KHR_external_fence/_fd, which + this function's "optional extensions independent of winsys" list and + its per-platform required-extensions list both also add unconditionally). + The result is a genuine duplicate extension name in + ppEnabledExtensionNames, which VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-01387 + forbids. Fixed by skipping anything extensions_to_enable already + contains in both places before adding it. + +2. The layer forwards the application's VkDeviceCreateInfo essentially + unmodified aside from the extension list. Some applications (again, + confirmed live for RetroArch's Vulkan HW render context negotiation) + set a legacy non-NULL pEnabledFeatures *and* chain a + VkPhysicalDeviceFeatures2 in pNext at the same time, which + VUID-VkDeviceCreateInfo-pNext-00373 forbids. Fixed by clearing + pEnabledFeatures in create_device() whenever a VkPhysicalDeviceFeatures2 + is also present in pNext - the pNext version is a strict superset of + what the legacy struct could express, so dropping the redundant pointer + is always safe. + +Verified live: libretro ppsspp_libretro.so (RetroArch) with video_driver=vulkan +went from an immediate SIGABRT (vkCreateDevice returning +VK_ERROR_INITIALIZATION_FAILED, silently ignored by the core, followed by +the Khronos loader's own validation catching the resulting invalid device +handle on the first vkGetDeviceQueue call) to a full clean run: device +created, Vulkan surface + 3-image swapchain created, FIFO present mode +selected, no dmesg GPU faults. This is a generic fix in the layer's own +device-creation path, independent of windowing system, so it should also +benefit any other libretro core using RetroArch's Vulkan HW render +negotiation path (parallel_n64/ParaLLEl-RDP, MAME with Vulkan support, +etc.), not just PPSSPP. +--- +--- a/layer/layer.cpp 2026-08-15 12:23:48.026222154 +0900 ++++ b/layer/layer.cpp +@@ -310,6 +310,24 @@ + } + } + ++ /* Some applications (confirmed live: RetroArch's Vulkan HW render context ++ * negotiation, used by libretro cores such as ppsspp_libretro) build a ++ * VkDeviceCreateInfo with a legacy non-NULL pEnabledFeatures *and* a ++ * chained VkPhysicalDeviceFeatures2 in pNext at the same time - the ++ * Vulkan spec (VUID-VkDeviceCreateInfo-pNext-00373) forbids this, and ++ * this Mali blob ICD rejects the whole vkCreateDevice call outright with ++ * VK_ERROR_INITIALIZATION_FAILED rather than tolerating/ignoring the ++ * redundant legacy pointer the way some other drivers do. Since the ++ * pNext-chained VkPhysicalDeviceFeatures2 is the modern, complete ++ * superset of what pEnabledFeatures could express, it's always safe to ++ * just drop the legacy pointer here when both are present. */ ++ if (modified_info.pEnabledFeatures != nullptr && ++ util::find_extension(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, ++ modified_info.pNext) != nullptr) ++ { ++ modified_info.pEnabledFeatures = nullptr; ++ } ++ + /* Now call create device on the chain further down the list. */ + TRY_LOG(fpCreateDevice(physicalDevice, &modified_info, pAllocator, pDevice), "Failed to create the device"); + +--- a/wsi/wsi_factory.cpp 2026-08-15 12:23:49.662243358 +0900 ++++ b/wsi/wsi_factory.cpp +@@ -175,6 +175,18 @@ + + for (auto extension : optional_extensions) + { ++ /* The Vulkan spec (VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-01387) ++ * forbids the same extension name appearing twice in ++ * ppEnabledExtensionNames. extensions_to_enable was seeded above with ++ * whatever the application already requested (e.g. RetroArch's own ++ * Vulkan HW render context negotiation already requests ++ * VK_KHR_external_fence/_fd itself) - skip anything already present ++ * instead of appending a duplicate, which this Mali blob ICD rejects ++ * outright with VK_ERROR_INITIALIZATION_FAILED (confirmed live). */ ++ if (extensions_to_enable.contains(extension)) ++ { ++ continue; ++ } + if (available_device_extensions.contains(extension)) + { + TRY_LOG_CALL(extensions_to_enable.add(extension)); +@@ -211,7 +223,21 @@ + return VK_ERROR_INITIALIZATION_FAILED; + } + +- TRY_LOG_CALL(extensions_to_enable.add(extensions_required_by_layer)); ++ /* Filter out anything the application already requested before merging: the Vulkan spec ++ * (VUID-VkDeviceCreateInfo-ppEnabledExtensionNames-01387) forbids duplicate extension ++ * names in ppEnabledExtensionNames, and this Mali blob ICD rejects such a device create ++ * request outright with VK_ERROR_INITIALIZATION_FAILED (confirmed live - RetroArch's own ++ * Vulkan HW render context negotiation already requests VK_KHR_external_fence/_fd, which ++ * also appear in this platform's required-extensions list above). */ ++ util::vector required_by_layer_strings{ allocator }; ++ TRY_LOG_CALL(extensions_required_by_layer.get_extension_strings(required_by_layer_strings)); ++ for (const char *extension : required_by_layer_strings) ++ { ++ if (!extensions_to_enable.contains(extension)) ++ { ++ TRY_LOG_CALL(extensions_to_enable.add(extension)); ++ } ++ } + } + + return VK_SUCCESS; From f9a7c59dc4fb099778c4170abd1144e4e36fd424 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 13:51:26 +0900 Subject: [PATCH 025/110] libretro-dolphin: enable Vulkan backend when BATOCERA_VULKAN is set Was building without -DENABLE_VULKAN at all - confirmed live via the libretro dolphin_libretro.so core's own maps (only libmali.so loaded, no libvulkan.so, "context type: gl" in the RetroArch log) that this core has never actually attempted Vulkan on this board, GLES-only. ROCKNIX's dolphin-lr package.mk enables Vulkan unconditionally whenever VULKAN_SUPPORT=yes (no ARM/x86 device gating, unlike their parallel-n64-lr which is x86_64-only for HAVE_PARALLEL - see next commit's investigation) - this core's Vulkan backend is a real, generically-available option upstream, not an x86-only experimental one. Matches the same BR2_PACKAGE_BATOCERA_VULKAN gate the standalone dolphin-emu package already uses. Not yet rebuilt/verified live - queued for the next full image build alongside the standalone dolphin-emu Vulkan fix (which was stale-built before BATOCERA_VULKAN was turned on) and PCSX2/vkquake (already BR2_PACKAGE_*=y in .config/.board but never actually built into the image that's currently flashed on the device). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../libretro/libretro-dolphin/libretro-dolphin.mk | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/package/batocera/emulators/retroarch/libretro/libretro-dolphin/libretro-dolphin.mk b/package/batocera/emulators/retroarch/libretro/libretro-dolphin/libretro-dolphin.mk index b123050d0d3..39eeeccf1be 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-dolphin/libretro-dolphin.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-dolphin/libretro-dolphin.mk @@ -34,6 +34,17 @@ else LIBRETRO_DOLPHIN_CONF_OPTS += -DENABLE_X11=OFF endif +# ROCKNIX's dolphin-lr package.mk enables this unconditionally whenever +# VULKAN_SUPPORT=yes (no ARM/x86 gating, unlike their parallel-n64-lr) - this +# core's Vulkan backend is a genuine, generically-available option, not an +# x86-only experimental one. Matches the same BR2_PACKAGE_BATOCERA_VULKAN +# gate the standalone dolphin-emu package already uses. +ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN),y) + LIBRETRO_DOLPHIN_CONF_OPTS += -DENABLE_VULKAN=ON +else + LIBRETRO_DOLPHIN_CONF_OPTS += -DENABLE_VULKAN=OFF +endif + define LIBRETRO_DOLPHIN_INSTALL_TARGET_CMDS $(INSTALL) -D $(@D)/dolphin_libretro.so \ $(TARGET_DIR)/usr/lib/libretro/dolphin_libretro.so From 11f3bb974788a5b7d67ff73ebec8ddad96cc54fd Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 13:59:34 +0900 Subject: [PATCH 026/110] board: disable duckstation - mesa3d Config.in gap breaks the build with BATOCERA_VULKAN=y duckstation.mk's BR2_PACKAGE_BATOCERA_VULKAN branch adds a real BR2_PACKAGE_MESA3D (not mesa3d-headers) dependency, but duckstation's own Config.in never selects/depends-on it - buildroot's own legality check fails the whole board build the instant BATOCERA_VULKAN=y (confirmed live: "mesa3d is in the dependency chain of duckstation that has added it to its _DEPENDENCIES variable without selecting it or depending on it from Config.in"). This branch was presumably always dead code on this board until now, since nothing had turned BATOCERA_VULKAN on before. Not worth chasing further: duckstation-qt was already abandoned on A/B for a separate, unrelated reason (Qt/eglfs can't host its multi-widget window structure without a compositor - "Failed to get window info from widget", see project memory), and PSX is already covered by libretro swanstation (the actual recommended core, 58fps+ software-rendered, no GPU faults). A real BR2_PACKAGE_MESA3D dependency would also risk the same class of Kconfig conflict this board already hit once with Saturn kronos (this board only configures BR2_PACKAGE_MESA3D_HEADERS for the blob, not full mesa3d). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index dc6b4821185..5300bb2acd2 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -196,6 +196,20 @@ BR2_PACKAGE_LIBRETRO_PPSSPP=y # platform wiring), libretro-ps2/libretro-play (CMake arch-detection # failure / not in es_systems.yml). See B's commits 04c0e462f6/5f1cf74e76. +# standalone duckstation - defaults on upstream, but its +# BR2_PACKAGE_BATOCERA_VULKAN branch adds a real BR2_PACKAGE_MESA3D +# dependency that its own Config.in never selects/depends-on (buildroot's +# legality check fails the whole board build the moment BATOCERA_VULKAN=y, +# confirmed live once we turned that flag on for Track C). Also already +# abandoned on A/B for a separate, unrelated reason (Qt/eglfs can't host +# duckstation-qt's multi-widget window structure without a compositor - +# "Failed to get window info from widget", see project memory) - PSX is +# already covered by libretro swanstation (the actual recommended core, +# 58fps+ software-rendered). Disabling here avoids a real mesa3d/blob +# Kconfig conflict (same class of problem as Saturn kronos, see B track) +# rather than trying to make an already-abandoned frontend build. +# BR2_PACKAGE_DUCKSTATION is not set + # Firmware BR2_PACKAGE_FIRMWARE_ARMBIAN=y From 2bc3f558a5e678fbcbce54d7871f73f1af580e5d Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:02:42 +0900 Subject: [PATCH 027/110] duckstation: fix disable - a defconfig "is not set" can't override batocera-system's select Previous commit's "# BR2_PACKAGE_DUCKSTATION is not set" line in the board defconfig had no effect: batocera-system/Config.in unconditionally selects DUCKSTATION for GLES3 boards, and Kconfig select always wins over a plain defconfig negation - confirmed live, .config still showed BR2_PACKAGE_DUCKSTATION=y after regenerating and the build hit the identical mesa3d error again. Fixed at the actual mechanism: added a depends-on guard directly to duckstation/Config.in itself. Kconfig silently drops a select whose target's own depends-on evaluates false (same non-fatal WARNING class already seen for the pre-existing GStreamer OpenGL dependency warnings in this board's build log) - this is the standard way to locally override a select from shared/upstream Config.in without touching that shared file. Condition matches the real conflict: BR2_PACKAGE_MESA3D_HEADERS (this board's vendor-blob setup) together with BR2_PACKAGE_BATOCERA_VULKAN (which makes duckstation.mk pull in full BR2_PACKAGE_MESA3D). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- configs/batocera-rk3568-odroidm1.board | 20 +++++++++---------- .../batocera/emulators/duckstation/Config.in | 14 +++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 5300bb2acd2..b7935052453 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -196,19 +196,19 @@ BR2_PACKAGE_LIBRETRO_PPSSPP=y # platform wiring), libretro-ps2/libretro-play (CMake arch-detection # failure / not in es_systems.yml). See B's commits 04c0e462f6/5f1cf74e76. -# standalone duckstation - defaults on upstream, but its -# BR2_PACKAGE_BATOCERA_VULKAN branch adds a real BR2_PACKAGE_MESA3D -# dependency that its own Config.in never selects/depends-on (buildroot's -# legality check fails the whole board build the moment BATOCERA_VULKAN=y, -# confirmed live once we turned that flag on for Track C). Also already -# abandoned on A/B for a separate, unrelated reason (Qt/eglfs can't host +# standalone duckstation - defaults on for GLES3 boards via +# batocera-system/Config.in's unconditional select, which a plain +# "is not set" here can't override. Same class of mesa3d/blob Kconfig +# conflict as Saturn kronos above once BATOCERA_VULKAN is on (its .mk +# needs full BR2_PACKAGE_MESA3D, which hard-conflicts with this board's +# BR2_PACKAGE_MESA3D_HEADERS-only blob setup) - fixed at the real source +# with a depends-on guard in duckstation/Config.in itself (Kconfig drops +# a select whose target's depends-on is unmet). Also already abandoned +# on A/B for a separate, unrelated reason (Qt/eglfs can't host # duckstation-qt's multi-widget window structure without a compositor - # "Failed to get window info from widget", see project memory) - PSX is # already covered by libretro swanstation (the actual recommended core, -# 58fps+ software-rendered). Disabling here avoids a real mesa3d/blob -# Kconfig conflict (same class of problem as Saturn kronos, see B track) -# rather than trying to make an already-abandoned frontend build. -# BR2_PACKAGE_DUCKSTATION is not set +# 58fps+ software-rendered). # Firmware BR2_PACKAGE_FIRMWARE_ARMBIAN=y diff --git a/package/batocera/emulators/duckstation/Config.in b/package/batocera/emulators/duckstation/Config.in index 5e4f5e6e884..a48c0a34cdb 100644 --- a/package/batocera/emulators/duckstation/Config.in +++ b/package/batocera/emulators/duckstation/Config.in @@ -1,5 +1,19 @@ config BR2_PACKAGE_DUCKSTATION bool "duckstation" + # Same class of conflict as Saturn kronos (see project_odroid_retro + # memory / Track C board notes): this package's own .mk pulls in a + # real BR2_PACKAGE_MESA3D dependency once BATOCERA_VULKAN is on, but + # mesa3d-headers.mk (what vendor-blob boards like this one use + # instead of full Mesa) hard-stops the whole build if both are + # enabled at once ("mesa3d-headers enabled, but mesa3d enabled + # too"). batocera-system/Config.in unconditionally selects this + # package for GLES3 boards, and a plain "is not set" in a board + # defconfig cannot override a select - only a failed depends-on can, + # which is what this line does (Kconfig silently drops a select + # whose target's own depends-on is unmet, at most a WARNING, same as + # the harmless GStreamer OpenGL warnings already seen on this + # board's build). + depends on !(BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN) select BR2_PACKAGE_DUCKSTATION_COMMON select BR2_PACKAGE_BATOCERA_QT6 select BR2_PACKAGE_FMT From f1be9dc0b8aebbcfda691c12c5db98fe92d91927 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:04:52 +0900 Subject: [PATCH 028/110] duckstation: actually fix the disable - override the shared select condition The depends-on guard from the previous commit didn't work: Kconfig's select overrides a target's own failed depends-on with only a WARNING (same class as this build's pre-existing, harmless GStreamer OpenGL dependency warnings) - confirmed live, .config still showed BR2_PACKAGE_DUCKSTATION=y after regenerating, and the build hit the identical class of error again for a second dependency (cpuinfo, after mesa3d), since the package was never actually disabled. Real fix: add the exclusion condition directly to batocera-system/Config.in's own select line, following the exact same pattern already used there for other board-specific exclusions (!BR2_PACKAGE_BATOCERA_TARGET_RK3326, !..._S9GEN4). Can't key off this board's own BR2_PACKAGE_BATOCERA_TARGET_RK3568 flag since that's shared across all three A/B/C GPU-driver tracks on this same physical board - only Track C has BR2_PACKAGE_BATOCERA_VULKAN on, so gating on BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN together is both the precise real conflict condition and inert on A/B. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../batocera/core/batocera-system/Config.in | 8 ++++++- .../batocera/emulators/duckstation/Config.in | 22 +++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package/batocera/core/batocera-system/Config.in b/package/batocera/core/batocera-system/Config.in index fc04f6ca05d..f19ff673f46 100644 --- a/package/batocera/core/batocera-system/Config.in +++ b/package/batocera/core/batocera-system/Config.in @@ -1455,7 +1455,13 @@ config BR2_PACKAGE_BATOCERA_CONSOLE_SYSTEMS # PSX select BR2_PACKAGE_DUCKSTATION if BR2_PACKAGE_BATOCERA_GLES3 && \ !BR2_PACKAGE_BATOCERA_TARGET_RK3326 && \ - !BR2_PACKAGE_BATOCERA_TARGET_S9GEN4 + !BR2_PACKAGE_BATOCERA_TARGET_S9GEN4 && \ + # duckstation.mk pulls in full BR2_PACKAGE_MESA3D once + # BATOCERA_VULKAN is on, which mesa3d-headers.mk (used + # by vendor-blob boards instead of full Mesa) hard-stops + # the build if combined with - see ODROID-M1 Track C + # board notes / project memory for the live conflict. + !(BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN) # newer versions of duckstation are stricter on OpenGL 3.1 # requires X11 or Sway. 2GB boards aren't great. diff --git a/package/batocera/emulators/duckstation/Config.in b/package/batocera/emulators/duckstation/Config.in index a48c0a34cdb..1c39f3c44b1 100644 --- a/package/batocera/emulators/duckstation/Config.in +++ b/package/batocera/emulators/duckstation/Config.in @@ -1,19 +1,13 @@ config BR2_PACKAGE_DUCKSTATION bool "duckstation" - # Same class of conflict as Saturn kronos (see project_odroid_retro - # memory / Track C board notes): this package's own .mk pulls in a - # real BR2_PACKAGE_MESA3D dependency once BATOCERA_VULKAN is on, but - # mesa3d-headers.mk (what vendor-blob boards like this one use - # instead of full Mesa) hard-stops the whole build if both are - # enabled at once ("mesa3d-headers enabled, but mesa3d enabled - # too"). batocera-system/Config.in unconditionally selects this - # package for GLES3 boards, and a plain "is not set" in a board - # defconfig cannot override a select - only a failed depends-on can, - # which is what this line does (Kconfig silently drops a select - # whose target's own depends-on is unmet, at most a WARNING, same as - # the harmless GStreamer OpenGL warnings already seen on this - # board's build). - depends on !(BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN) + # Vendor-blob boards with BATOCERA_VULKAN on (this package's .mk then + # needs full BR2_PACKAGE_MESA3D, which conflicts with + # BR2_PACKAGE_MESA3D_HEADERS - see Saturn kronos precedent) are + # excluded via batocera-system/Config.in's own select condition, not + # a depends-on here: buildroot's Kconfig `select` overrides a failed + # `depends on` on the target with only a warning (confirmed live - + # same as this build's pre-existing, harmless GStreamer OpenGL + # dependency warnings), so a guard on this side has no real effect. select BR2_PACKAGE_DUCKSTATION_COMMON select BR2_PACKAGE_BATOCERA_QT6 select BR2_PACKAGE_FMT From fdbd09a0ec2e8a643f08e6b41dd1e59d40ab96b1 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:12:56 +0900 Subject: [PATCH 029/110] batocera-system: fix Kconfig syntax error from mid-expression comments Comments interleaved inside the backslash-continued DUCKSTATION select if-expression broke the Kconfig parser (it treated the comment as ending the statement early, then choked on the leftover !(...) as a bogus new statement). Move the explanatory comment above the select line instead; the exclusion condition itself is unchanged. --- package/batocera/core/batocera-system/Config.in | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package/batocera/core/batocera-system/Config.in b/package/batocera/core/batocera-system/Config.in index f19ff673f46..6cde1e9b8a2 100644 --- a/package/batocera/core/batocera-system/Config.in +++ b/package/batocera/core/batocera-system/Config.in @@ -1453,17 +1453,16 @@ config BR2_PACKAGE_BATOCERA_CONSOLE_SYSTEMS select BR2_PACKAGE_BATOCERA_SEGADC # PSX + # duckstation.mk pulls in full BR2_PACKAGE_MESA3D once BATOCERA_VULKAN is + # on, which mesa3d-headers.mk (used by vendor-blob boards instead of full + # Mesa) hard-stops the build if combined with - see ODROID-M1 Track C + # board notes / project memory for the live conflict. + # newer versions of duckstation are stricter on OpenGL 3.1 + # requires X11 or Sway. 2GB boards aren't great. select BR2_PACKAGE_DUCKSTATION if BR2_PACKAGE_BATOCERA_GLES3 && \ !BR2_PACKAGE_BATOCERA_TARGET_RK3326 && \ !BR2_PACKAGE_BATOCERA_TARGET_S9GEN4 && \ - # duckstation.mk pulls in full BR2_PACKAGE_MESA3D once - # BATOCERA_VULKAN is on, which mesa3d-headers.mk (used - # by vendor-blob boards instead of full Mesa) hard-stops - # the build if combined with - see ODROID-M1 Track C - # board notes / project memory for the live conflict. !(BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN) - # newer versions of duckstation are stricter on OpenGL 3.1 - # requires X11 or Sway. 2GB boards aren't great. select BR2_PACKAGE_DUCKSTATION_LEGACY if !BR2_PACKAGE_BATOCERA_GLES3 && \ !BR2_riscv && \ From 36faefd2cc6d02c23b0a722f7e68004fa81efa69 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:16:46 +0900 Subject: [PATCH 030/110] sdl3: don't pull in full mesa3d for Vulkan on mesa3d-headers boards SDL3_DEPENDENCIES unconditionally added mesa3d whenever BATOCERA_VULKAN=y, even though SDL's Vulkan support only actually needs vulkan-loader/vulkan-headers. On vendor-blob boards using mesa3d-headers instead of full mesa3d (ODROID-M1 Track C), this tripped buildroot's package-legality check (mesa3d symbol not =y) since mesa3d-headers and mesa3d are mutually exclusive - same class of conflict as the earlier duckstation fix. --- package/batocera/libraries/sdl3/sdl3.mk | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package/batocera/libraries/sdl3/sdl3.mk b/package/batocera/libraries/sdl3/sdl3.mk index 754675a3910..b2ccb22c258 100644 --- a/package/batocera/libraries/sdl3/sdl3.mk +++ b/package/batocera/libraries/sdl3/sdl3.mk @@ -68,7 +68,14 @@ SDL3_CONF_OPTS += -DSDL_LIBUDEV=OFF endif ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN),y) -SDL3_DEPENDENCIES += mesa3d vulkan-loader +SDL3_DEPENDENCIES += vulkan-loader +# mesa3d itself is only needed here when it's the actual Vulkan/GL provider; +# vendor-blob boards (BR2_PACKAGE_MESA3D_HEADERS) supply Vulkan via their own +# ICD + vulkan-loader and are incompatible with building full mesa3d +# alongside mesa3d-headers (see ODROID-M1 Track C notes). +ifneq ($(BR2_PACKAGE_MESA3D_HEADERS),y) +SDL3_DEPENDENCIES += mesa3d +endif SDL3_CONF_OPTS += -DSDL_VULKAN=ON SDL3_CONF_OPTS += -DSDL_RENDER_VULKAN=ON else From 5cf49d0d819e508e4c75b8131c057db384680c11 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:30:33 +0900 Subject: [PATCH 031/110] dolphin-emu: switch to ROCKNIX's Wayland+Vulkan patch series on Wayland boards Root cause (confirmed by direct source read earlier this session): the currently pinned upstream commit's Vulkan backend never defines VK_USE_PLATFORM_WAYLAND_KHR and has no Wayland branch anywhere in VulkanLoader.h/VKSwapChain.cpp/VulkanContext.cpp - Win32/Xlib/Android/ Metal only. This is a genuine upstream gap, not a config or WSI-layer bug: verified current dolphin-emu/dolphin master (2026-08-14) still has zero Wayland references anywhere in the Vulkan backend or CMakeLists.txt. ROCKNIX hits the identical gap on their own Wayland+Vulkan RK3566 hardware (mali blob + sway/wlroots, architecturally the same as this board) and carries a real out-of-tree fix: a newer pinned commit (e6583f8bec814d8f3748f1d7738457600ce0de56) plus a ~5400-line patch series that adds native Wayland EGL/Vulkan surface support end-to-end (VulkanLoader.h platform macro, VKSwapChain.cpp surface creation, VulkanContext.cpp extensions, a new DolphinNoGUI Wayland Platform, plus supporting CMake find-modules). Verified locally: all 6 patches apply with plain `patch -p1` against a fresh clone at that exact commit, zero fuzz beyond the housekeeping patches (padorder/git/bios-dir) - this is what ROCKNIX actually ships and runs, not a guess. Trade-off, noted for the record: e6583f8b predates this board's current pin by ~3 years (Feb 2023 vs Jun 2026), and RetroAchievements support does not exist in that source tree at all (USE_RETRO_ACHIEVEMENTS isn't a recognized CMake option there - the flag is silently ignored, not a build error). Confirmed via `git log` that upstream dolphin-emu/dolphin still has never merged native Wayland support even at today's HEAD, so there is no newer/cleaner base to pin to instead - this ROCKNIX-patched older commit is the only known working prior art for this combo. Wired up: - dolphin-emu.mk: version/patch-dir switch and CMake flags gated on BR2_PACKAGE_BATOCERA_WAYLAND (forces NoGUI, since the patch only wires Wayland into DolphinNoGUI's Platform, not the Qt GUI - confirmed zero Qt files touched by the patch). - Patches live in a version-scoped subdirectory (e6583f8bec814d8f3748f1d7738457600ce0de56/) per buildroot's own pkg-patches-dirs convention, so they only apply to that exact pinned version and never touch the non-Wayland-board build path. - dolphinGenerator.py: dolphin-emu-nogui needs an explicit --platform (unlike Qt, it doesn't probe WAYLAND_DISPLAY/DISPLAY itself and defaults to x11), plus --video_backend for Vulkan/OGL selection. Not yet live-verified on hardware - queued as the next full rebuild's validation target. --- .../generators/dolphin/dolphinGenerator.py | 7 +- .../emulators/dolphin-emu/dolphin-emu.mk | 36 +- .../000-add-wayland.patch | 5370 +++++++++++++++++ .../001-padorder.patch | 111 + .../002-fix-bba.patch | 46 + .../004-git.patch | 13 + .../005-hide-osd-msg.patch | 13 + .../008-bios-dir.patch | 35 + 8 files changed, 5628 insertions(+), 3 deletions(-) create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/000-add-wayland.patch create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/001-padorder.patch create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/002-fix-bba.patch create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/004-git.patch create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/005-hide-osd-msg.patch create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/008-bios-dir.patch diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py index 8b466e4e2e9..6bcf168bcd0 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/dolphin/dolphinGenerator.py @@ -423,7 +423,12 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game # use the -b 'batch' option for nicer exit commandArray = ["dolphin-emu", "-b", "-e", rom] else: - commandArray = ["dolphin-emu-nogui", "-e", rom] + # NoGUI-only build (Wayland boards - see dolphin-emu.mk): needs an + # explicit --platform, it doesn't probe WAYLAND_DISPLAY/DISPLAY + # itself like Qt does, and defaults to x11 if left unset. + nogui_platform = "wayland" if environ.get("WAYLAND_DISPLAY") else "x11" + nogui_video_backend = dolphinSettings.get("Core", "GFXBackend") + commandArray = ["dolphin-emu-nogui", "-p", nogui_platform, "-v", nogui_video_backend, "-e", rom] # state_slot option if state_filename := system.config.get('state_filename'): diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index 3d822b47693..0b4422dfec9 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -3,11 +3,24 @@ # dolphin-emu # ################################################################################ +# On Wayland boards, the stock commit below has no VK_USE_PLATFORM_WAYLAND_KHR +# support at all (confirmed via source read: VulkanLoader.h/VKSwapChain.cpp +# only handle Win32/Xlib/Android/Metal) - Vulkan surface creation fails +# outright under labwc/Wayland. ROCKNIX hits the exact same gap on their own +# Wayland+Vulkan RK3566 devices and solves it by pinning a newer upstream +# commit plus their own "add-wayland" patch series (verified: applies clean +# with `patch -p1`, no fuzz beyond padorder/git/bios-dir housekeeping patches +# - see ODROID-M1 Track C notes). We mirror that combo here rather than +# hand-porting the patch onto the older commit. +ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) +DOLPHIN_EMU_VERSION = e6583f8bec814d8f3748f1d7738457600ce0de56 +else # Version: Commits on Jun 25, 2026 # Add major & minor version accordingly for any bump DOLPHIN_EMU_VERSION = 6094cfcf7b8fba733b3116fdf3414d51c1c0e4a4 DOLPHIN_EMU_VERSION_MAJOR = 2606 DOLPHIN_EMU_VERSION_MINOR = +endif DOLPHIN_EMU_SITE = https://github.com/dolphin-emu/dolphin DOLPHIN_EMU_SITE_METHOD = git DOLPHIN_EMU_LICENSE = GPLv2+ @@ -17,6 +30,10 @@ DOLPHIN_EMU_SUPPORTS_IN_SOURCE_BUILD = NO DOLPHIN_EMU_DEPENDENCIES = libevdev ffmpeg zlib libpng lzo libusb libcurl DOLPHIN_EMU_DEPENDENCIES += bluez5_utils hidapi xz host-xz sdl3 cpp-ipc +ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) +DOLPHIN_EMU_DEPENDENCIES += wayland wayland-protocols libxkbcommon ecm +endif + $(eval $(call register,dolphin.emulator.yml)) $(eval $(call register-if-kconfig,BR2_PACKAGE_BATOCERA_VULKAN,gfxbackend.dolphin.emulator.yml)) @@ -34,6 +51,15 @@ DOLPHIN_EMU_CONF_OPTS += -DUSE_SYSTEM_FMT=OFF DOLPHIN_EMU_CONF_OPTS += -DENABLE_CLI_TOOL=OFF DOLPHIN_EMU_CONF_OPTS += -DUSE_RETRO_ACHIEVEMENTS=ON +ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) +# The Wayland patch series only wires native Wayland support into +# DolphinNoGUI's Platform abstraction, not into the Qt GUI's window widget +# (confirmed: patch touches zero Qt source files) - so Wayland boards must +# use the NoGUI/dolphin-emu-nogui binary regardless of Qt6 availability. +DOLPHIN_EMU_CONF_OPTS += -DENABLE_QT=OFF +DOLPHIN_EMU_CONF_OPTS += -DENABLE_NOGUI=ON +DOLPHIN_EMU_CONF_OPTS += -DENABLE_WAYLAND=ON +else ifeq ($(BR2_PACKAGE_QT6),y) DOLPHIN_EMU_DEPENDENCIES += qt6base qt6svg DOLPHIN_EMU_CONF_OPTS += -DENABLE_QT=ON @@ -42,6 +68,7 @@ else DOLPHIN_EMU_CONF_OPTS += -DENABLE_QT=OFF DOLPHIN_EMU_CONF_OPTS += -DENABLE_NOGUI=ON endif +endif DOLPHIN_EMU_MAKE_ENV += LDFLAGS="-Wl,--copy-dt-needed-entries" DOLPHIN_EMU_CONF_ENV += LDFLAGS="-Wl,--copy-dt-needed-entries" @@ -58,22 +85,27 @@ else DOLPHIN_EMU_CONF_OPTS += -DENABLE_VULKAN=OFF endif +ifneq ($(DOLPHIN_EMU_VERSION_MAJOR),) define DOLPHIN_EMU_PRE_CONFIGURE_HOOK sed -i 's/set(DOLPHIN_VERSION_MAJOR .*)/set(DOLPHIN_VERSION_MAJOR "$(DOLPHIN_EMU_VERSION_MAJOR)")/' \ $(@D)/CMake/ScmRevGen.cmake sed -i 's/set(DOLPHIN_VERSION_MINOR .*)/set(DOLPHIN_VERSION_MINOR "$(DOLPHIN_EMU_VERSION_MINOR)")/' \ $(@D)/CMake/ScmRevGen.cmake endef +DOLPHIN_EMU_PRE_CONFIGURE_HOOKS = DOLPHIN_EMU_PRE_CONFIGURE_HOOK +endif define DOLPHIN_EMU_INI mkdir -p $(TARGET_DIR)/usr/share/dolphin-emu/sys/GameSettings # copy extra triforce ini files - force overwrite cp -pr $(BR2_EXTERNAL_BATOCERA_PATH)/package/batocera/emulators/dolphin-emu/*.ini \ $(TARGET_DIR)/usr/share/dolphin-emu/sys/GameSettings - cd $(TARGET_DIR)/usr/bin && ln -sf dolphin-emu dolphin-emu.desktopconfig + # NoGUI-only builds (Wayland boards) never install a dolphin-emu Qt binary + if [ -f $(TARGET_DIR)/usr/bin/dolphin-emu ]; then \ + cd $(TARGET_DIR)/usr/bin && ln -sf dolphin-emu dolphin-emu.desktopconfig; \ + fi endef -DOLPHIN_EMU_PRE_CONFIGURE_HOOKS = DOLPHIN_EMU_PRE_CONFIGURE_HOOK DOLPHIN_EMU_POST_INSTALL_TARGET_HOOKS += DOLPHIN_EMU_INI $(eval $(cmake-package)) diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/000-add-wayland.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/000-add-wayland.patch new file mode 100644 index 00000000000..ffbb76dd7f3 --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/000-add-wayland.patch @@ -0,0 +1,5370 @@ +diff -rupN dolphin.orig/CMake/FindWaylandProtocols.cmake dolphin/CMake/FindWaylandProtocols.cmake +--- dolphin.orig/CMake/FindWaylandProtocols.cmake 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/CMake/FindWaylandProtocols.cmake 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,28 @@ ++# from https://github.com/glfw/glfw/blob/master/CMake/modules/FindWaylandProtocols.cmake ++ ++find_package(PkgConfig) ++ ++pkg_check_modules(WaylandProtocols QUIET wayland-protocols>=${WaylandProtocols_FIND_VERSION}) ++ ++execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=pkgdatadir wayland-protocols ++ OUTPUT_VARIABLE WaylandProtocols_PKGDATADIR ++ RESULT_VARIABLE _pkgconfig_failed) ++if (_pkgconfig_failed) ++ message(FATAL_ERROR "Missing wayland-protocols pkgdatadir") ++endif() ++ ++string(REGEX REPLACE "[\r\n]" "" WaylandProtocols_PKGDATADIR "${WaylandProtocols_PKGDATADIR}") ++ ++find_package_handle_standard_args(WaylandProtocols ++ FOUND_VAR ++ WaylandProtocols_FOUND ++ REQUIRED_VARS ++ WaylandProtocols_PKGDATADIR ++ VERSION_VAR ++ WaylandProtocols_VERSION ++ HANDLE_COMPONENTS ++) ++ ++set(WAYLAND_PROTOCOLS_FOUND ${WaylandProtocols_FOUND}) ++set(WAYLAND_PROTOCOLS_PKGDATADIR ${WaylandProtocols_PKGDATADIR}) ++set(WAYLAND_PROTOCOLS_VERSION ${WaylandProtocols_VERSION}) +diff -rupN dolphin.orig/CMake/FindXKBCommon.cmake dolphin/CMake/FindXKBCommon.cmake +--- dolphin.orig/CMake/FindXKBCommon.cmake 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/CMake/FindXKBCommon.cmake 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,33 @@ ++# - Try to find XKBCommon ++# Once done, this will define ++# ++# XKBCOMMON_FOUND - System has XKBCommon ++# XKBCOMMON_INCLUDE_DIRS - The XKBCommon include directories ++# XKBCOMMON_LIBRARIES - The libraries needed to use XKBCommon ++# XKBCOMMON_DEFINITIONS - Compiler switches required for using XKBCommon ++ ++find_package(PkgConfig) ++pkg_check_modules(PC_XKBCOMMON QUIET xkbcommon) ++set(XKBCOMMON_DEFINITIONS ${PC_XKBCOMMON_CFLAGS_OTHER}) ++ ++find_path(XKBCOMMON_INCLUDE_DIR ++ NAMES xkbcommon/xkbcommon.h ++ HINTS ${PC_XKBCOMMON_INCLUDE_DIR} ${PC_XKBCOMMON_INCLUDE_DIRS} ++) ++ ++find_library(XKBCOMMON_LIBRARY ++ NAMES xkbcommon ++ HINTS ${PC_XKBCOMMON_LIBRARY} ${PC_XKBCOMMON_LIBRARY_DIRS} ++) ++ ++set(XKBCOMMON_LIBRARIES ${XKBCOMMON_LIBRARY}) ++set(XKBCOMMON_LIBRARY_DIRS ${XKBCOMMON_LIBRARY_DIRS}) ++set(XKBCOMMON_INCLUDE_DIRS ${XKBCOMMON_INCLUDE_DIR}) ++ ++include(FindPackageHandleStandardArgs) ++find_package_handle_standard_args(XKBCommon DEFAULT_MSG ++ XKBCOMMON_LIBRARY ++ XKBCOMMON_INCLUDE_DIR ++) ++ ++mark_as_advanced(XKBCOMMON_LIBRARY XKBCOMMON_INCLUDE_DIR) +diff -rupN dolphin.orig/CMakeLists.txt dolphin/CMakeLists.txt +--- dolphin.orig/CMakeLists.txt 2024-03-05 04:05:25.878960469 +0000 ++++ dolphin/CMakeLists.txt 2024-03-05 04:06:08.240295843 +0000 +@@ -47,6 +47,7 @@ set(DOLPHIN_DEFAULT_UPDATE_TRACK "" CACH + + if(UNIX AND NOT APPLE AND NOT ANDROID) + option(ENABLE_X11 "Enables X11 Support" ON) ++ option(ENABLE_WAYLAND "Enables Wayland Support" OFF) + endif() + if(NOT WIN32 AND NOT APPLE AND NOT HAIKU) + option(ENABLE_EGL "Enables EGL OpenGL Interface" ON) +@@ -543,6 +544,17 @@ if(ENABLE_X11) + endif() + endif() + ++if(ENABLE_WAYLAND) ++ find_package(ECM REQUIRED NO_MODULE) ++ list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}") ++ find_package(Wayland REQUIRED Client Egl) ++ find_package(WaylandScanner REQUIRED) ++ find_package(WaylandProtocols 1.15 REQUIRED) ++ find_package(XKBCommon REQUIRED) ++ add_definitions(-DHAVE_WAYLAND=1) ++ message(STATUS "Wayland support enabled") ++endif() ++ + if(ENABLE_EGL) + find_package(EGL) + if(EGL_FOUND) +diff -rupN dolphin.orig/Source/Core/Common/CMakeLists.txt dolphin/Source/Core/Common/CMakeLists.txt +--- dolphin.orig/Source/Core/Common/CMakeLists.txt 2024-03-05 04:05:26.574982411 +0000 ++++ dolphin/Source/Core/Common/CMakeLists.txt 2024-03-05 04:06:08.240295843 +0000 +@@ -253,11 +253,20 @@ if(ENABLE_EGL AND EGL_FOUND) + GL/GLInterface/EGLAndroid.cpp + GL/GLInterface/EGLAndroid.h + ) +- elseif(ENABLE_X11 AND X11_FOUND) +- target_sources(common PRIVATE +- GL/GLInterface/EGLX11.cpp +- GL/GLInterface/EGLX11.h +- ) ++ else() ++ if(ENABLE_X11 AND X11_FOUND) ++ target_sources(common PRIVATE ++ GL/GLInterface/EGLX11.cpp ++ GL/GLInterface/EGLX11.h ++ ) ++ endif() ++ if(ENABLE_WAYLAND AND WAYLAND_FOUND) ++ target_sources(common PRIVATE ++ GL/GLInterface/EGLWayland.cpp ++ GL/GLInterface/EGLWayland.h ++ ) ++ target_link_libraries(common PRIVATE Wayland::Egl) ++ endif() + endif() + target_include_directories(common PRIVATE ${EGL_INCLUDE_DIRS}) + target_link_libraries(common PUBLIC ${EGL_LIBRARIES}) +diff -rupN dolphin.orig/Source/Core/Common/GL/GLContext.cpp dolphin/Source/Core/Common/GL/GLContext.cpp +--- dolphin.orig/Source/Core/Common/GL/GLContext.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLContext.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -25,6 +25,9 @@ + #if defined(ANDROID) + #include "Common/GL/GLInterface/EGLAndroid.h" + #endif ++#if HAVE_WAYLAND ++#include "Common/GL/GLInterface/EGLWayland.h" ++#endif + #endif + + const std::array, 9> GLContext::s_desktop_opengl_versions = { +@@ -57,11 +60,11 @@ bool GLContext::ClearCurrent() + return false; + } + +-void GLContext::Update() ++void GLContext::UpdateDimensions(int window_width, int window_height) + { + } + +-void GLContext::UpdateSurface(void* window_handle) ++void GLContext::UpdateSurface(void* window_handle, int window_width, int window_height) + { + } + +@@ -113,6 +116,10 @@ std::unique_ptr GLContext::Cr + #endif + } + #endif ++#if HAVE_WAYLAND ++ if (wsi.type == WindowSystemType::Wayland) ++ context = std::make_unique(); ++#endif + #if HAVE_EGL + if (wsi.type == WindowSystemType::Headless || wsi.type == WindowSystemType::FBDev) + context = std::make_unique(); +diff -rupN dolphin.orig/Source/Core/Common/GL/GLContext.h dolphin/Source/Core/Common/GL/GLContext.h +--- dolphin.orig/Source/Core/Common/GL/GLContext.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLContext.h 2024-03-05 04:06:08.240295843 +0000 +@@ -36,8 +36,8 @@ public: + virtual bool MakeCurrent(); + virtual bool ClearCurrent(); + +- virtual void Update(); +- virtual void UpdateSurface(void* window_handle); ++ virtual void UpdateDimensions(int window_width, int window_height); ++ virtual void UpdateSurface(void* window_handle, int window_width, int window_height); + + virtual void Swap(); + virtual void SwapInterval(int interval); +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/AGL.h dolphin/Source/Core/Common/GL/GLInterface/AGL.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/AGL.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/AGL.h 2024-03-05 04:06:08.240295843 +0000 +@@ -27,6 +27,8 @@ public: + + bool MakeCurrent() override; + bool ClearCurrent() override; ++ ++ void UpdateDimensions(int window_width, int window_height) override; + + void Update() override; + +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/AGL.mm dolphin/Source/Core/Common/GL/GLInterface/AGL.mm +--- dolphin.orig/Source/Core/Common/GL/GLInterface/AGL.mm 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/AGL.mm 2024-03-05 04:06:08.240295843 +0000 +@@ -144,7 +144,7 @@ bool GLContextAGL::ClearCurrent() + return true; + } + +-void GLContextAGL::Update() ++void GLContextAGL::UpdateDimensions(int window_width, int window_height) + { + if (!m_view) + return; +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGL.cpp dolphin/Source/Core/Common/GL/GLInterface/EGL.cpp +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGL.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGL.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -292,8 +292,8 @@ bool GLContextEGL::CreateWindowSurface() + { + if (!IsHeadless()) + { +- EGLNativeWindowType native_window = GetEGLNativeWindow(m_config); +- m_egl_surface = eglCreateWindowSurface(m_egl_display, m_config, native_window, nullptr); ++ m_native_window = GetEGLNativeWindow(m_config); ++ m_egl_surface = eglCreateWindowSurface(m_egl_display, m_config, m_native_window, nullptr); + if (!m_egl_surface) + { + INFO_LOG_FMT(VIDEO, "Error: eglCreateWindowSurface failed"); +@@ -301,15 +301,7 @@ bool GLContextEGL::CreateWindowSurface() + } + + // Get dimensions from the surface. +- EGLint surface_width = 1, surface_height = 1; +- if (!eglQuerySurface(m_egl_display, m_egl_surface, EGL_WIDTH, &surface_width) || +- !eglQuerySurface(m_egl_display, m_egl_surface, EGL_HEIGHT, &surface_height)) +- { +- WARN_LOG_FMT(VIDEO, +- "Failed to get surface dimensions via eglQuerySurface. Size may be incorrect."); +- } +- m_backbuffer_width = static_cast(surface_width); +- m_backbuffer_height = static_cast(surface_height); ++ QueryDimensions(); + } + else if (!m_supports_surfaceless) + { +@@ -347,9 +339,16 @@ bool GLContextEGL::MakeCurrent() + return eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context); + } + +-void GLContextEGL::UpdateSurface(void* window_handle) ++void GLContextEGL::UpdateDimensions(int window_width, int window_height) ++{ ++ QueryDimensions(); ++} ++ ++void GLContextEGL::UpdateSurface(void* window_handle, int window_width, int window_height) + { + m_wsi.render_surface = window_handle; ++ m_wsi.render_surface_width = window_width; ++ m_wsi.render_surface_height = window_height; + ClearCurrent(); + DestroyWindowSurface(); + CreateWindowSurface(); +@@ -376,3 +375,15 @@ void GLContextEGL::DestroyContext() + m_egl_context = EGL_NO_CONTEXT; + m_egl_display = EGL_NO_DISPLAY; + } ++ ++void GLContextEGL::QueryDimensions() ++{ ++ EGLint surface_width = 1, surface_height = 1; ++ if (!eglQuerySurface(m_egl_display, m_egl_surface, EGL_WIDTH, &surface_width) || ++ !eglQuerySurface(m_egl_display, m_egl_surface, EGL_HEIGHT, &surface_height)) ++ { ++ INFO_LOG_FMT(VIDEO, "Failed to get surface dimensions via eglQuerySurface. Size may be incorrect."); ++ } ++ m_backbuffer_width = static_cast(surface_width); ++ m_backbuffer_height = static_cast(surface_height); ++} +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGL.h dolphin/Source/Core/Common/GL/GLInterface/EGL.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGL.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGL.h 2024-03-05 04:06:08.240295843 +0000 +@@ -22,7 +22,8 @@ public: + bool MakeCurrent() override; + bool ClearCurrent() override; + +- void UpdateSurface(void* window_handle) override; ++ void UpdateDimensions(int window_width, int window_height); ++ void UpdateSurface(void* window_handle, int window_width, int window_height) override; + + void Swap() override; + void SwapInterval(int interval) override; +@@ -39,9 +40,12 @@ protected: + void DestroyWindowSurface(); + void DetectMode(); + void DestroyContext(); ++ void QueryDimensions(); + + WindowSystemInfo m_wsi = {}; + ++ EGLNativeWindowType m_native_window = {}; ++ + EGLConfig m_config; + bool m_supports_surfaceless = false; + std::vector m_attribs; +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGLWayland.cpp dolphin/Source/Core/Common/GL/GLInterface/EGLWayland.cpp +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGLWayland.cpp 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGLWayland.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,36 @@ ++// Copyright 2019 Dolphin Emulator Project ++// Licensed under GPLv2+ ++// Refer to the license.txt file included. ++ ++#include "Common/GL/GLInterface/EGLWayland.h" ++#include ++ ++GLContextEGLWayland::~GLContextEGLWayland() ++{ ++ if (m_native_window) ++ wl_egl_window_destroy(reinterpret_cast(m_native_window)); ++} ++ ++EGLDisplay GLContextEGLWayland::OpenEGLDisplay() ++{ ++ return eglGetDisplay(reinterpret_cast(m_wsi.display_connection)); ++} ++ ++void GLContextEGLWayland::UpdateDimensions(int window_width, int window_height) ++{ ++ wl_egl_window_resize(reinterpret_cast(m_native_window), window_width, ++ window_height, 0, 0); ++ m_backbuffer_width = window_width; ++ m_backbuffer_height = window_height; ++} ++ ++EGLNativeWindowType GLContextEGLWayland::GetEGLNativeWindow(EGLConfig config) ++{ ++ wl_egl_window* window = ++ wl_egl_window_create(static_cast(m_wsi.render_surface), ++ m_wsi.render_surface_width, m_wsi.render_surface_height); ++ if (!window) ++ return {}; ++ ++ return reinterpret_cast(window); ++} +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGLWayland.h dolphin/Source/Core/Common/GL/GLInterface/EGLWayland.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGLWayland.h 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGLWayland.h 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,19 @@ ++// Copyright 2019 Dolphin Emulator Project ++// Licensed under GPLv2+ ++// Refer to the license.txt file included. ++ ++#pragma once ++ ++#include "Common/GL/GLInterface/EGL.h" ++ ++class GLContextEGLWayland : public GLContextEGL ++{ ++public: ++ ~GLContextEGLWayland(); ++ ++ void UpdateDimensions(int window_width, int window_height) override; ++ ++protected: ++ EGLDisplay OpenEGLDisplay() override; ++ EGLNativeWindowType GetEGLNativeWindow(EGLConfig config) override; ++}; +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGLX11.cpp dolphin/Source/Core/Common/GL/GLInterface/EGLX11.cpp +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGLX11.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGLX11.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -11,7 +11,7 @@ GLContextEGLX11::~GLContextEGLX11() + m_render_window.reset(); + } + +-void GLContextEGLX11::Update() ++void GLContextEGLX11::UpdateDimensions(int window_width, int window_height) + { + m_render_window->UpdateDimensions(); + m_backbuffer_width = m_render_window->GetWidth(); +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/EGLX11.h dolphin/Source/Core/Common/GL/GLInterface/EGLX11.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/EGLX11.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/EGLX11.h 2024-03-05 04:06:08.240295843 +0000 +@@ -13,7 +13,7 @@ class GLContextEGLX11 final : public GLC + public: + ~GLContextEGLX11() override; + +- void Update() override; ++void UpdateDimensions(int window_width, int window_height) override; + + protected: + EGLDisplay OpenEGLDisplay() override; +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/GLX.cpp dolphin/Source/Core/Common/GL/GLInterface/GLX.cpp +--- dolphin.orig/Source/Core/Common/GL/GLInterface/GLX.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/GLX.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -310,7 +310,7 @@ bool GLContextGLX::ClearCurrent() + return glXMakeCurrent(m_display, None, nullptr); + } + +-void GLContextGLX::Update() ++void GLContextGLX::UpdateDimensions(int window_width, int window_height) + { + m_render_window->UpdateDimensions(); + m_backbuffer_width = m_render_window->GetWidth(); +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/GLX.h dolphin/Source/Core/Common/GL/GLInterface/GLX.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/GLX.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/GLX.h 2024-03-05 04:06:08.240295843 +0000 +@@ -24,7 +24,7 @@ public: + bool MakeCurrent() override; + bool ClearCurrent() override; + +- void Update() override; ++ void UpdateDimensions(int window_width, int window_height) override; + + void SwapInterval(int Interval) override; + void Swap() override; +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/WGL.cpp dolphin/Source/Core/Common/GL/GLInterface/WGL.cpp +--- dolphin.orig/Source/Core/Common/GL/GLInterface/WGL.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/WGL.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -480,7 +480,7 @@ bool GLContextWGL::ClearCurrent() + } + + // Update window width, size and etc. Called from Render.cpp +-void GLContextWGL::Update() ++void GLContextWGL::UpdateDimensions(int window_width, int window_height) + { + RECT rcWindow; + GetClientRect(m_window_handle, &rcWindow); +diff -rupN dolphin.orig/Source/Core/Common/GL/GLInterface/WGL.h dolphin/Source/Core/Common/GL/GLInterface/WGL.h +--- dolphin.orig/Source/Core/Common/GL/GLInterface/WGL.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/GL/GLInterface/WGL.h 2024-03-05 04:06:08.240295843 +0000 +@@ -19,7 +19,7 @@ public: + bool MakeCurrent() override; + bool ClearCurrent() override; + +- void Update() override; ++ void UpdateDimensions(int window_width, int window_height) override; + + void Swap() override; + void SwapInterval(int interval) override; +diff -rupN dolphin.orig/Source/Core/Common/WindowSystemInfo.h dolphin/Source/Core/Common/WindowSystemInfo.h +--- dolphin.orig/Source/Core/Common/WindowSystemInfo.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Common/WindowSystemInfo.h 2024-03-05 04:06:08.240295843 +0000 +@@ -40,7 +40,11 @@ struct WindowSystemInfo + // This is kept seperate as input may require a different handle to rendering, and + // during video backend startup the surface pointer may change (MoltenVK). + void* render_surface = nullptr; +- ++ ++ // Dimensions of the render surface, if this is determined by the frontend. ++ int render_surface_width = 0; ++ int render_surface_height = 0; ++ + // Scale of the render surface. For hidpi systems, this will be >1. + float render_surface_scale = 1.0f; + }; +diff -rupN dolphin.orig/Source/Core/Core/BootManager.cpp dolphin/Source/Core/Core/BootManager.cpp +--- dolphin.orig/Source/Core/Core/BootManager.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Core/BootManager.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -64,6 +64,11 @@ bool BootCore(std::unique_ptr(boot->parameters)) ++ { ++ StartUp.SetBaseSaveState(PathToFileName(std::get(boot->parameters).path)); ++ } ++ + // Movie settings + if (Movie::IsPlayingInput() && Movie::IsConfigSaved()) + { +diff -rupN dolphin.orig/Source/Core/Core/ConfigManager.cpp dolphin/Source/Core/Core/ConfigManager.cpp +--- dolphin.orig/Source/Core/Core/ConfigManager.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Core/ConfigManager.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -139,6 +139,10 @@ void SConfig::SetRunningGameMetadata(con + SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown); + } + ++void SConfig::SetBaseSaveState(const std::string& base_save_state) { ++ m_base_save_state = base_save_state; ++} ++ + void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id, + u64 title_id, u16 revision, DiscIO::Region region) + { +diff -rupN dolphin.orig/Source/Core/Core/ConfigManager.h dolphin/Source/Core/Core/ConfigManager.h +--- dolphin.orig/Source/Core/Core/ConfigManager.h 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Core/ConfigManager.h 2024-03-05 04:06:08.240295843 +0000 +@@ -56,6 +56,8 @@ struct SConfig + // TODO: remove this as soon as the ticket view hack in IOS/ES/Views is dropped. + bool m_disc_booted_from_game_list = false; + ++ const std::string& GetBaseSaveState() const { return m_base_save_state; } ++ void SetBaseSaveState(const std::string& base_save_state); + const std::string& GetGameID() const { return m_game_id; } + const std::string& GetGameTDBID() const { return m_gametdb_id; } + const std::string& GetTitleName() const { return m_title_name; } +@@ -109,6 +111,7 @@ private: + + static SConfig* m_Instance; + ++ std::string m_base_save_state; + std::string m_game_id; + std::string m_gametdb_id; + std::string m_title_name; +diff -rupN dolphin.orig/Source/Core/Core/Core.cpp dolphin/Source/Core/Core/Core.cpp +--- dolphin.orig/Source/Core/Core/Core.cpp 2024-03-05 04:05:26.578982537 +0000 ++++ dolphin/Source/Core/Core/Core.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -474,6 +474,8 @@ static void EmuThread(std::unique_ptrAddInput(ControllerEmu::Translate, START_BUTTON, _trans("START")); + ++ // Hotkey Button ++ m_buttons->AddInput(ControllerEmu::Translate, HOTKEY_BUTTON, _trans("HOTKEY")); ++ + // sticks + groups.emplace_back(m_main_stick = new ControllerEmu::OctagonAnalogStick( + MAIN_STICK_GROUP, _trans("Control Stick"), MAIN_STICK_GATE_RADIUS)); +diff -rupN dolphin.orig/Source/Core/Core/HW/GCPadEmu.h dolphin/Source/Core/Core/HW/GCPadEmu.h +--- dolphin.orig/Source/Core/Core/HW/GCPadEmu.h 2024-03-05 04:05:26.582982663 +0000 ++++ dolphin/Source/Core/Core/HW/GCPadEmu.h 2024-03-05 14:38:20.524919664 +0000 +@@ -66,6 +66,8 @@ public: + static constexpr const char* Y_BUTTON = "Y"; + static constexpr const char* Z_BUTTON = "Z"; + static constexpr const char* START_BUTTON = "Start"; ++ static constexpr const char* HOTKEY_BUTTON = "Hotkey"; ++ + + // i18n: The left trigger button (labeled L on real controllers) + static constexpr const char* L_DIGITAL = _trans("L"); +diff -rupN dolphin.orig/Source/Core/Core/State.cpp dolphin/Source/Core/Core/State.cpp +--- dolphin.orig/Source/Core/Core/State.cpp 2024-03-05 04:05:26.586982789 +0000 ++++ dolphin/Source/Core/Core/State.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -327,7 +327,10 @@ static std::string SystemTimeAsDoubleToS + #endif + } + +-static std::string MakeStateFilename(int number); ++static std::string MakeStateFilename(bool useId, int number); ++static std::string MakeStateFilename_partdirectory(int number); ++static std::string MakeStateFilename_partfileName(int number); ++static std::string MakeStateFilename_partfileID(int number); + + // read state timestamps + static std::map GetSavedStates() +@@ -336,7 +339,7 @@ static std::map GetSavedSta + std::map m; + for (int i = 1; i <= (int)NUM_STATES; i++) + { +- std::string filename = MakeStateFilename(i); ++ std::string filename = MakeStateFilename(true, i); + if (File::Exists(filename)) + { + if (ReadHeader(filename, header)) +@@ -536,7 +539,7 @@ bool ReadHeader(const std::string& filen + + std::string GetInfoStringOfSlot(int slot, bool translate) + { +- std::string filename = MakeStateFilename(slot); ++ std::string filename = MakeStateFilename(true, slot); + if (!File::Exists(filename)) + return translate ? Common::GetStringT("Empty") : "Empty"; + +@@ -550,7 +553,7 @@ std::string GetInfoStringOfSlot(int slot + u64 GetUnixTimeOfSlot(int slot) + { + State::StateHeader header; +- if (!ReadHeader(MakeStateFilename(slot), header)) ++ if (!ReadHeader(MakeStateFilename(true, slot), header)) + return 0; + + constexpr u64 MS_PER_SEC = 1000; +@@ -751,20 +754,48 @@ void Shutdown() + } + } + +-static std::string MakeStateFilename(int number) ++static std::string MakeStateFilename(bool useId, int number) + { +- return fmt::format("{}{}.s{:02d}", File::GetUserPath(D_STATESAVES_IDX), +- SConfig::GetInstance().GetGameID(), number); ++ // if the useId flag is set : if the file with name exits, returns it, otherwise, if the file with id exists, returns it, otherwise, return the file with name ++ // => priority with the file with name. ++ // if the game is not loaded from a file (disk), use the gameId ++ std::string filePartName = MakeStateFilename_partfileName(number); ++ std::string fileWithName = fmt::format("{}{}", MakeStateFilename_partdirectory(number), filePartName); ++ if (File::Exists(fileWithName) && filePartName != "") ++ return fileWithName; ++ std::string fileWithId = fmt::format("{}{}", MakeStateFilename_partdirectory(number), MakeStateFilename_partfileID(number)); ++ if (File::Exists(fileWithId) && useId) ++ return fileWithId; ++ if(filePartName == "") return fileWithId; ++ return fileWithName; ++} ++ ++static std::string MakeStateFilename_partdirectory(int number) ++{ ++ return File::GetUserPath(D_STATESAVES_IDX); ++} ++ ++static std::string MakeStateFilename_partfileID(int number) ++{ ++ return fmt::format("{}.s{:02d}", SConfig::GetInstance().GetGameID(), number); ++} ++ ++static std::string MakeStateFilename_partfileName(int number) ++{ ++ std::string basesavestate = SConfig::GetInstance().GetBaseSaveState(); ++ if(basesavestate == "") return ""; ++ return fmt::format("{}.s{:02d}", basesavestate, number); + } + + void Save(int slot, bool wait) + { +- SaveAs(MakeStateFilename(slot), wait); ++ std::string savePath = MakeStateFilename(false, slot); ++ SaveAs(savePath, wait); + } + + void Load(int slot) + { +- LoadAs(MakeStateFilename(slot)); ++ LoadAs(MakeStateFilename(true, slot)); + } + + void LoadLastSaved(int i) +diff -rupN dolphin.orig/Source/Core/DolphinLib.props dolphin/Source/Core/DolphinLib.props +--- dolphin.orig/Source/Core/DolphinLib.props 2024-03-05 04:05:26.590982915 +0000 ++++ dolphin/Source/Core/DolphinLib.props 2024-03-05 04:06:08.240295843 +0000 +@@ -1207,6 +1207,7 @@ + + + ++ + + + +diff -rupN dolphin.orig/Source/Core/DolphinNoGUI/CMakeLists.txt dolphin/Source/Core/DolphinNoGUI/CMakeLists.txt +--- dolphin.orig/Source/Core/DolphinNoGUI/CMakeLists.txt 2024-03-05 04:05:26.590982915 +0000 ++++ dolphin/Source/Core/DolphinNoGUI/CMakeLists.txt 2024-03-05 04:06:08.240295843 +0000 +@@ -17,6 +17,22 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" + target_sources(dolphin-nogui PRIVATE PlatformFBDev.cpp) + endif() + ++if(ENABLE_WAYLAND AND WAYLAND_FOUND) ++ set(WAYLAND_PLATFORM_SRCS PlatformWayland.cpp) ++ ++ # Generate the xdg-shell and xdg-decoration protocols at build-time. ++ ecm_add_wayland_client_protocol(WAYLAND_PLATFORM_SRCS ++ PROTOCOL "${WAYLAND_PROTOCOLS_PKGDATADIR}/stable/xdg-shell/xdg-shell.xml" ++ BASENAME xdg-shell) ++ ecm_add_wayland_client_protocol(WAYLAND_PLATFORM_SRCS ++ PROTOCOL "${WAYLAND_PROTOCOLS_PKGDATADIR}/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" ++ BASENAME xdg-decoration) ++ ++ target_include_directories(dolphin-nogui PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") ++ target_sources(dolphin-nogui PRIVATE "${WAYLAND_PLATFORM_SRCS}") ++ target_link_libraries(dolphin-nogui PRIVATE Wayland::Client) ++endif() ++ + set_target_properties(dolphin-nogui PROPERTIES OUTPUT_NAME dolphin-emu-nogui) + + target_link_libraries(dolphin-nogui +diff -rupN dolphin.orig/Source/Core/DolphinNoGUI/MainNoGUI.cpp dolphin/Source/Core/DolphinNoGUI/MainNoGUI.cpp +--- dolphin.orig/Source/Core/DolphinNoGUI/MainNoGUI.cpp 2024-03-05 04:05:26.590982915 +0000 ++++ dolphin/Source/Core/DolphinNoGUI/MainNoGUI.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -155,6 +155,11 @@ static std::unique_ptr GetPlat + { + std::string platform_name = static_cast(options.get("platform")); + ++#if HAVE_WAYLAND ++ if (platform_name == "wayland") ++ return Platform::CreateWaylandPlatform(); ++#endif ++ + #if HAVE_X11 + if (platform_name == "x11" || platform_name.empty()) + return Platform::CreateX11Platform(); +@@ -200,6 +205,10 @@ int main(int argc, char* argv[]) + , + "win32" + #endif ++#ifdef HAVE_WAYLAND ++ , ++ "wayland" ++#endif + }); + + optparse::Values& options = CommandLineParse::ParseArguments(parser.get(), argc, argv); +diff -rupN dolphin.orig/Source/Core/DolphinNoGUI/Platform.h dolphin/Source/Core/DolphinNoGUI/Platform.h +--- dolphin.orig/Source/Core/DolphinNoGUI/Platform.h 2024-03-05 04:05:26.590982915 +0000 ++++ dolphin/Source/Core/DolphinNoGUI/Platform.h 2024-03-05 04:06:08.240295843 +0000 +@@ -35,6 +35,10 @@ public: + static std::unique_ptr CreateX11Platform(); + #endif + ++#ifdef HAVE_WAYLAND ++ static std::unique_ptr CreateWaylandPlatform(); ++#endif ++ + #ifdef __linux__ + static std::unique_ptr CreateFBDevPlatform(); + #endif +diff -rupN dolphin.orig/Source/Core/DolphinNoGUI/PlatformWayland.cpp dolphin/Source/Core/DolphinNoGUI/PlatformWayland.cpp +--- dolphin.orig/Source/Core/DolphinNoGUI/PlatformWayland.cpp 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/Source/Core/DolphinNoGUI/PlatformWayland.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,364 @@ ++// Copyright 2018 Dolphin Emulator Project ++// Licensed under GPLv2+ ++// Refer to the license.txt file included. ++ ++#include ++ ++#include "DolphinNoGUI/Platform.h" ++ ++#include "Common/MsgHandler.h" ++#include "Core/Config/MainSettings.h" ++#include "Core/Core.h" ++#include "Core/State.h" ++ ++#include "Core/HW/GCPad.h" ++#include "InputCommon/GCPadStatus.h" ++#include ++#include "Core/Config/GraphicsSettings.h" ++#include "VideoCommon/VideoConfig.h" ++#include "VideoCommon/OnScreenDisplay.h" ++ ++#include ++#include ++#include ++ ++#include ++#include "wayland-xdg-decoration-client-protocol.h" ++#include "wayland-xdg-shell-client-protocol.h" ++ ++#include "UICommon/X11Utils.h" ++#include "VideoCommon/RenderBase.h" ++ ++namespace ++{ ++class PlatformWayland : public Platform ++{ ++public: ++ ~PlatformWayland() override; ++ ++ bool Init() override; ++ void SetTitle(const std::string& string) override; ++ void MainLoop() override; ++ ++ WindowSystemInfo GetWindowSystemInfo() const; ++ ++private: ++ void ProcessEvents(); ++ ++ static void GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, ++ const char* interface, uint32_t version); ++ static void GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id); ++ static void XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial); ++ static void XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, uint32_t serial); ++ static void TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width, ++ int32_t height, struct wl_array* states); ++ static void TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel); ++ ++ wl_display* m_display = nullptr; ++ wl_registry* m_registry = nullptr; ++ wl_compositor* m_compositor = nullptr; ++ xdg_wm_base* m_xdg_wm_base = nullptr; ++ wl_surface* m_surface = nullptr; ++ wl_region* m_region = nullptr; ++ xdg_surface* m_xdg_surface = nullptr; ++ xdg_toplevel* m_xdg_toplevel = nullptr; ++ zxdg_decoration_manager_v1* m_decoration_manager = nullptr; ++ zxdg_toplevel_decoration_v1* m_toplevel_decoration = nullptr; ++ ++ int m_surface_width = 0; ++ int m_surface_height = 0; ++}; ++ ++PlatformWayland::~PlatformWayland() ++{ ++ if (m_xdg_toplevel) ++ xdg_toplevel_destroy(m_xdg_toplevel); ++ if (m_xdg_surface) ++ xdg_surface_destroy(m_xdg_surface); ++ if (m_surface) ++ wl_surface_destroy(m_surface); ++ if (m_region) ++ wl_region_destroy(m_region); ++ if (m_xdg_wm_base) ++ xdg_wm_base_destroy(m_xdg_wm_base); ++ if (m_compositor) ++ wl_compositor_destroy(m_compositor); ++ if (m_registry) ++ wl_registry_destroy(m_registry); ++ if (m_display) ++ wl_display_disconnect(m_display); ++} ++ ++void PlatformWayland::GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, ++ const char* interface, uint32_t version) ++{ ++ PlatformWayland* platform = static_cast(data); ++ if (std::strcmp(interface, wl_compositor_interface.name) == 0) ++ { ++ platform->m_compositor = static_cast( ++ wl_registry_bind(platform->m_registry, id, &wl_compositor_interface, 1)); ++ } ++ else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) ++ { ++ platform->m_xdg_wm_base = static_cast( ++ wl_registry_bind(platform->m_registry, id, &xdg_wm_base_interface, 1)); ++ } ++ else if (std::strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) ++ { ++ platform->m_decoration_manager = static_cast( ++ wl_registry_bind(platform->m_registry, id, &zxdg_decoration_manager_v1_interface, 1)); ++ } ++} ++ ++void PlatformWayland::GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id) ++{ ++} ++ ++void PlatformWayland::XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial) ++{ ++ xdg_wm_base_pong(xdg_wm_base, serial); ++} ++ ++void PlatformWayland::XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, ++ uint32_t serial) ++{ ++ xdg_surface_ack_configure(xdg_surface, serial); ++} ++ ++void PlatformWayland::TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, ++ int32_t width, int32_t height, struct wl_array* states) ++{ ++ // If this is zero, it's asking us to set the size. ++ if (width == 0 || height == 0) ++ return; ++ ++ PlatformWayland* platform = static_cast(data); ++ platform->m_surface_width = width; ++ platform->m_surface_height = height; ++ if (g_renderer) ++ g_renderer->ResizeSurface(width, height); ++ //if (g_controller_interface.IsInit()) ++ //g_controller_interface.OnWindowResized(width, height); ++} ++ ++void PlatformWayland::TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel) ++{ ++ PlatformWayland* platform = static_cast(data); ++ platform->Stop(); ++} ++ ++bool PlatformWayland::Init() ++{ ++ m_display = wl_display_connect(nullptr); ++ if (!m_display) ++ { ++ //PanicAlert("Failed to connect to Wayland display."); ++ return false; ++ } ++ ++ static const wl_registry_listener registry_listener = {GlobalRegistryHandler, ++ GlobalRegistryRemover}; ++ m_registry = wl_display_get_registry(m_display); ++ wl_registry_add_listener(m_registry, ®istry_listener, this); ++ ++ // Call back to registry listener to get compositor/shell. ++ wl_display_dispatch(m_display); ++ wl_display_roundtrip(m_display); ++ ++ // We need a shell/compositor, or at least one we understand. ++ if (!m_compositor || !m_display || !m_xdg_wm_base) ++ { ++ std::fprintf(stderr, "Missing Wayland shell/compositor\n"); ++ return false; ++ } ++ ++ // Create the compositor and shell surface. ++ if (!(m_surface = wl_compositor_create_surface(m_compositor)) || ++ !(m_xdg_surface = xdg_wm_base_get_xdg_surface(m_xdg_wm_base, m_surface)) || ++ !(m_xdg_toplevel = xdg_surface_get_toplevel(m_xdg_surface))) ++ { ++ std::fprintf(stderr, "Failed to create compositor/shell surfaces\n"); ++ return false; ++ } ++ ++ static const xdg_wm_base_listener xdg_wm_base_listener = {XDGWMBasePing}; ++ xdg_wm_base_add_listener(m_xdg_wm_base, &xdg_wm_base_listener, this); ++ ++ static const xdg_surface_listener shell_surface_listener = {XDGSurfaceConfigure}; ++ xdg_surface_add_listener(m_xdg_surface, &shell_surface_listener, this); ++ ++ static const xdg_toplevel_listener toplevel_listener = {TopLevelConfigure, TopLevelClose}; ++ xdg_toplevel_add_listener(m_xdg_toplevel, &toplevel_listener, this); ++ ++ // Create region in the surface to draw into. ++ m_surface_width = Config::Get(Config::MAIN_RENDER_WINDOW_WIDTH); ++ m_surface_height = Config::Get(Config::MAIN_RENDER_WINDOW_HEIGHT); ++ m_region = wl_compositor_create_region(m_compositor); ++ wl_region_add(m_region, 0, 0, m_surface_width, m_surface_height); ++ wl_surface_set_opaque_region(m_surface, m_region); ++ wl_surface_commit(m_surface); ++ ++ // This doesn't seem to have any effect on kwin... ++ xdg_surface_set_window_geometry(m_xdg_surface, Config::Get(Config::MAIN_RENDER_WINDOW_XPOS), ++ Config::Get(Config::MAIN_RENDER_WINDOW_YPOS), ++ Config::Get(Config::MAIN_RENDER_WINDOW_WIDTH), ++ Config::Get(Config::MAIN_RENDER_WINDOW_HEIGHT)); ++ ++ if (m_decoration_manager) ++ { ++ m_toplevel_decoration = ++ zxdg_decoration_manager_v1_get_toplevel_decoration(m_decoration_manager, m_xdg_toplevel); ++ if (m_toplevel_decoration) ++ zxdg_toplevel_decoration_v1_set_mode(m_toplevel_decoration, ++ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); ++ } ++ ++ return true; ++} ++ ++void PlatformWayland::SetTitle(const std::string& string) ++{ ++ xdg_toplevel_set_title(m_xdg_toplevel, string.c_str()); ++} ++ ++void PlatformWayland::MainLoop() ++{ ++ while (IsRunning()) ++ { ++ static int hotkey = 0; ++ static int slot = 0; ++ static int fps = 0; ++ static int aspect = 0; ++ static int fforward = 0; ++ static int ires = 0; ++ ++ UpdateRunningFlag(); ++ Core::HostDispatchJobs(); ++ ProcessEvents(); ++ ++ if(Pad::IsInitialized()) { ++ GCPadStatus x = Pad::GetStatus(0); ++ ++ if( (x.button & PAD_BUTTON_HOTKEY) == PAD_BUTTON_HOTKEY) { // hotkey pressed ++ if(hotkey == 1) { ++ hotkey = 2; ++ } ++ } else { ++ hotkey = 1; // assure hotkey is released between actions ++ } ++ ++ if(hotkey == 2) { // hotkey pressed ++ if( (x.button & PAD_BUTTON_START) == PAD_BUTTON_START) { ++ RequestShutdown(); ++ hotkey = 0; ++ } ++ ++ if( (x.button & PAD_TRIGGER_L) == PAD_TRIGGER_L) { ++ State::Load(slot); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_TRIGGER_R) == PAD_TRIGGER_R) { ++ State::Save(slot); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_DOWN) == PAD_BUTTON_DOWN) { ++ if(slot > 0) slot--; ++ Core::DisplayMessage(fmt::format("Slot {} selected", slot), 4000); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_UP) == PAD_BUTTON_UP) { ++ if(slot < 10) slot++; ++ Core::DisplayMessage(fmt::format("Slot {} selected", slot), 4000); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_A) == PAD_BUTTON_A) { ++ Core::SaveScreenShot(); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_Y) == PAD_BUTTON_Y) { ++ if(fps == 0) { ++ Config::SetCurrent(Config::GFX_SHOW_FPS, True); ++ fps = 1; ++ } else { ++ Config::SetCurrent(Config::GFX_SHOW_FPS, False); ++ fps = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_X) == PAD_BUTTON_X) { ++ if(aspect == 0) { ++ Config::SetCurrent(Config::GFX_ASPECT_RATIO, AspectMode::Stretch); ++ aspect = 1; ++ } else { ++ Config::SetCurrent(Config::GFX_ASPECT_RATIO, AspectMode::Auto); ++ aspect = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_B) == PAD_BUTTON_B) { ++ if(ires == 0) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 2); ++ OSD::AddMessage("Internal Resolution: 480P"); ++ ires = 2; ++ } ++ else if(ires == 2) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 4); ++ OSD::AddMessage("Internal Resolution: 720P"); ++ ires = 4; ++ } ++ else if(ires == 4) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 6); ++ OSD::AddMessage("Internal Resolution: 1080P"); ++ ires = 6; ++ } else { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 1); ++ OSD::AddMessage("Internal Resolution: 240P"); ++ ires = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_TRIGGER_Z) == PAD_TRIGGER_Z) { ++ if(fforward == 0) { ++ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) + 1.0; ++ speed = (speed >= 0.95 && speed <= 1.05) ? 1.0 : speed; ++ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed); ++ OSD::AddMessage("Fast Forward: ON"); ++ fforward = 1; ++ } else { ++ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) - 1.0; ++ speed = (speed <= 0 || (speed >= 0.95 && speed <= 1.05)) ? 1.0 : speed; ++ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed); ++ OSD::AddMessage("Fast Forward: OFF"); ++ fforward = 0; ++ } ++ hotkey = 0; ++ } ++ } ++ } ++ ++ // TODO: Is this sleep appropriate? ++ std::this_thread::sleep_for(std::chrono::milliseconds(1)); ++ } ++} ++ ++WindowSystemInfo PlatformWayland::GetWindowSystemInfo() const ++{ ++ WindowSystemInfo wsi; ++ wsi.type = WindowSystemType::Wayland; ++ wsi.display_connection = static_cast(m_display); ++ wsi.render_surface = reinterpret_cast(m_surface); ++ wsi.render_surface_width = m_surface_width; ++ wsi.render_surface_height = m_surface_height; ++ return wsi; ++} ++ ++void PlatformWayland::ProcessEvents() ++{ ++ wl_display_dispatch_pending(m_display); ++} ++} // namespace ++ ++std::unique_ptr Platform::CreateWaylandPlatform() ++{ ++ return std::make_unique(); ++} +diff -rupN dolphin.orig/Source/Core/DolphinNoGUI/PlatformX11.cpp dolphin/Source/Core/DolphinNoGUI/PlatformX11.cpp +--- dolphin.orig/Source/Core/DolphinNoGUI/PlatformX11.cpp 2024-03-05 04:05:26.590982915 +0000 ++++ dolphin/Source/Core/DolphinNoGUI/PlatformX11.cpp 2024-03-05 04:21:20.855117890 +0000 +@@ -16,6 +16,13 @@ static constexpr auto X_None = None; + #include "Core/Core.h" + #include "Core/State.h" + ++#include "Core/HW/GCPad.h" ++#include "InputCommon/GCPadStatus.h" ++#include ++#include "Core/Config/GraphicsSettings.h" ++#include "VideoCommon/VideoConfig.h" ++#include "VideoCommon/OnScreenDisplay.h" ++ + #include + #include + #include +@@ -57,8 +64,8 @@ private: + #endif + int m_window_x = Config::Get(Config::MAIN_RENDER_WINDOW_XPOS); + int m_window_y = Config::Get(Config::MAIN_RENDER_WINDOW_YPOS); +- unsigned int m_window_width = Config::Get(Config::MAIN_RENDER_WINDOW_WIDTH); +- unsigned int m_window_height = Config::Get(Config::MAIN_RENDER_WINDOW_HEIGHT); ++ int m_window_width = Config::Get(Config::MAIN_RENDER_WINDOW_WIDTH); ++ int m_window_height = Config::Get(Config::MAIN_RENDER_WINDOW_HEIGHT); + }; + + PlatformX11::~PlatformX11() +@@ -149,11 +156,118 @@ void PlatformX11::MainLoop() + { + while (IsRunning()) + { ++ static int hotkey = 0; ++ static int slot = 0; ++ static int fps = 0; ++ static int aspect = 0; ++ static int fforward = 0; ++ static int ires = 0; ++ + UpdateRunningFlag(); + Core::HostDispatchJobs(); + ProcessEvents(); + UpdateWindowPosition(); + ++ if(Pad::IsInitialized()) { ++ GCPadStatus x = Pad::GetStatus(0); ++ ++ if( (x.button & PAD_BUTTON_HOTKEY) == PAD_BUTTON_HOTKEY) { // hotkey pressed ++ if(hotkey == 1) { ++ hotkey = 2; ++ } ++ } else { ++ hotkey = 1; // assure hotkey is released between actions ++ } ++ ++ if(hotkey == 2) { // hotkey pressed ++ if( (x.button & PAD_BUTTON_START) == PAD_BUTTON_START) { ++ RequestShutdown(); ++ hotkey = 0; ++ } ++ ++ if( (x.button & PAD_TRIGGER_L) == PAD_TRIGGER_L) { ++ State::Load(slot); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_TRIGGER_R) == PAD_TRIGGER_R) { ++ State::Save(slot); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_DOWN) == PAD_BUTTON_DOWN) { ++ if(slot > 0) slot--; ++ Core::DisplayMessage(fmt::format("Slot {} selected", slot), 4000); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_UP) == PAD_BUTTON_UP) { ++ if(slot < 10) slot++; ++ Core::DisplayMessage(fmt::format("Slot {} selected", slot), 4000); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_A) == PAD_BUTTON_A) { ++ Core::SaveScreenShot(); ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_Y) == PAD_BUTTON_Y) { ++ if(fps == 0) { ++ Config::SetCurrent(Config::GFX_SHOW_FPS, True); ++ fps = 1; ++ } else { ++ Config::SetCurrent(Config::GFX_SHOW_FPS, False); ++ fps = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_X) == PAD_BUTTON_X) { ++ if(aspect == 0) { ++ Config::SetCurrent(Config::GFX_ASPECT_RATIO, AspectMode::Stretch); ++ aspect = 1; ++ } else { ++ Config::SetCurrent(Config::GFX_ASPECT_RATIO, AspectMode::Auto); ++ aspect = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_BUTTON_B) == PAD_BUTTON_B) { ++ if(ires == 0) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 2); ++ OSD::AddMessage("Internal Resolution: 480P"); ++ ires = 2; ++ } ++ else if(ires == 2) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 4); ++ OSD::AddMessage("Internal Resolution: 720P"); ++ ires = 4; ++ } ++ else if(ires == 4) { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 6); ++ OSD::AddMessage("Internal Resolution: 1080P"); ++ ires = 6; ++ } else { ++ Config::SetCurrent(Config::GFX_EFB_SCALE, 1); ++ OSD::AddMessage("Internal Resolution: 240P"); ++ ires = 0; ++ } ++ hotkey = 0; ++ } ++ if( (x.button & PAD_TRIGGER_Z) == PAD_TRIGGER_Z) { ++ if(fforward == 0) { ++ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) + 1.0; ++ speed = (speed >= 0.95 && speed <= 1.05) ? 1.0 : speed; ++ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed); ++ OSD::AddMessage("Fast Forward: ON"); ++ fforward = 1; ++ } else { ++ auto speed = Config::Get(Config::MAIN_EMULATION_SPEED) - 1.0; ++ speed = (speed <= 0 || (speed >= 0.95 && speed <= 1.05)) ? 1.0 : speed; ++ Config::SetCurrent(Config::MAIN_EMULATION_SPEED, speed); ++ OSD::AddMessage("Fast Forward: OFF"); ++ fforward = 0; ++ } ++ hotkey = 0; ++ } ++ } ++ } ++ + // TODO: Is this sleep appropriate? + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +@@ -166,6 +280,8 @@ WindowSystemInfo PlatformX11::GetWindowS + wsi.display_connection = static_cast(m_display); + wsi.render_window = reinterpret_cast(m_window); + wsi.render_surface = reinterpret_cast(m_window); ++ wsi.render_surface_width = m_window_width; ++ wsi.render_surface_height = m_window_height; + return wsi; + } + +@@ -176,8 +292,9 @@ void PlatformX11::UpdateWindowPosition() + + Window winDummy; + unsigned int borderDummy, depthDummy; +- XGetGeometry(m_display, m_window, &winDummy, &m_window_x, &m_window_y, &m_window_width, +- &m_window_height, &borderDummy, &depthDummy); ++ XGetGeometry(m_display, m_window, &winDummy, &m_window_x, &m_window_y, ++ reinterpret_cast(&m_window_width), ++ reinterpret_cast(&m_window_height), &borderDummy, &depthDummy); + } + + void PlatformX11::ProcessEvents() +@@ -264,7 +381,10 @@ void PlatformX11::ProcessEvents() + case ConfigureNotify: + { + if (g_renderer) +- g_renderer->ResizeSurface(); ++ { ++ UpdateWindowPosition(); ++ g_renderer->ResizeSurface(m_window_width, m_window_height); ++ } + } + break; + } +diff -rupN dolphin.orig/Source/Core/InputCommon/GCPadStatus.h dolphin/Source/Core/InputCommon/GCPadStatus.h +--- dolphin.orig/Source/Core/InputCommon/GCPadStatus.h 2024-03-05 04:05:26.598983167 +0000 ++++ dolphin/Source/Core/InputCommon/GCPadStatus.h 2024-03-05 04:06:08.240295843 +0000 +@@ -26,6 +26,7 @@ enum PadButton + PAD_BUTTON_X = 0x0400, + PAD_BUTTON_Y = 0x0800, + PAD_BUTTON_START = 0x1000, ++ PAD_BUTTON_HOTKEY = 0x2000, + }; + + struct GCPadStatus +diff -rupN dolphin.orig/Source/Core/VideoBackends/OGL/OGLRender.cpp dolphin/Source/Core/VideoBackends/OGL/OGLRender.cpp +--- dolphin.orig/Source/Core/VideoBackends/OGL/OGLRender.cpp 2024-03-05 04:05:26.598983167 +0000 ++++ dolphin/Source/Core/VideoBackends/OGL/OGLRender.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -471,11 +471,7 @@ Renderer::Renderer(std::unique_ptrIsGLES()) + { +- g_ogl_config.SupportedESPointSize = GLExtensions::Supports("GL_OES_geometry_point_size") ? 1 : +- GLExtensions::Supports("GL_EXT_geometry_point_size") ? 2 : +- 0; ++ g_ogl_config.SupportedESPointSize = ++ GLExtensions::Supports("GL_OES_geometry_point_size") ? EsPointSizeType::PointSizeOes : ++ GLExtensions::Supports("GL_EXT_geometry_point_size") ? EsPointSizeType::PointSizeExt : ++ EsPointSizeType::PointSizeNone; + g_ogl_config.SupportedESTextureBuffer = + GLExtensions::Supports("VERSION_GLES_3_2") ? EsTexbufType::TexbufCore : + GLExtensions::Supports("GL_OES_texture_buffer") ? EsTexbufType::TexbufOes : +@@ -548,21 +545,16 @@ Renderer::Renderer(std::unique_ptr 0; ++ g_Config.backend_info.bSupportsGeometryShaders && ++ g_ogl_config.SupportedESPointSize != EsPointSizeType::PointSizeNone; + g_Config.backend_info.bSupportsSSAA = g_ogl_config.bSupportsAEP; + g_Config.backend_info.bSupportsFragmentStoresAndAtomics = true; + g_ogl_config.bSupportsMSAA = true; + g_ogl_config.bSupportsTextureStorage = true; +- g_ogl_config.bSupports2DTextureStorageMultisample = true; ++ if (GLExtensions::Supports("GL_OES_texture_storage_multisample_2d_array")) ++ g_ogl_config.SupportedMultisampleTexStorage = MultisampleTexStorageType::TexStorageOes; + g_Config.backend_info.bSupportsBitfield = true; + g_Config.backend_info.bSupportsDynamicSamplerIndexing = g_ogl_config.bSupportsAEP; +- if (g_ActiveConfig.stereo_mode != StereoMode::Off && g_ActiveConfig.iMultisamples > 1 && +- !g_ogl_config.bSupports3DTextureStorageMultisample) +- { +- // GLES 3.1 can't support stereo rendering and MSAA +- OSD::AddMessage("MSAA Stereo rendering isn't supported by your GPU.", 10000); +- Config::SetCurrent(Config::GFX_MSAA, UINT32_C(1)); +- } + } + else + { +@@ -572,7 +564,8 @@ Renderer::Renderer(std::unique_ptr 0; ++ g_Config.backend_info.bSupportsGSInstancing = ++ g_ogl_config.SupportedESPointSize != EsPointSizeType::PointSizeNone; + g_Config.backend_info.bSupportsPaletteConversion = true; + g_Config.backend_info.bSupportsSSAA = true; + g_Config.backend_info.bSupportsFragmentStoresAndAtomics = true; +@@ -581,8 +574,7 @@ Renderer::Renderer(std::unique_ptrUpdateSurface(m_new_surface_handle); ++ m_main_gl_context->UpdateSurface(m_new_surface_handle, m_new_surface_width, m_new_surface_height); + m_new_surface_handle = nullptr; + + // With a surface change, the window likely has new dimensions. +@@ -1093,7 +1088,7 @@ void Renderer::CheckForSurfaceResize() + if (!m_surface_resized.TestAndClear()) + return; + +- m_main_gl_context->Update(); ++ m_main_gl_context->UpdateDimensions(m_new_surface_width, m_new_surface_height); + m_backbuffer_width = m_main_gl_context->GetBackBufferWidth(); + m_backbuffer_height = m_main_gl_context->GetBackBufferHeight(); + m_system_framebuffer->UpdateDimensions(m_backbuffer_width, m_backbuffer_height); +diff -rupN dolphin.orig/Source/Core/VideoBackends/OGL/OGLRender.h dolphin/Source/Core/VideoBackends/OGL/OGLRender.h +--- dolphin.orig/Source/Core/VideoBackends/OGL/OGLRender.h 2024-03-05 04:05:26.598983167 +0000 ++++ dolphin/Source/Core/VideoBackends/OGL/OGLRender.h 2024-03-05 04:06:08.240295843 +0000 +@@ -31,6 +31,14 @@ enum GlslVersion + GlslEs310, // GLES 3.1 + GlslEs320, // GLES 3.2 + }; ++ ++enum class EsPointSizeType ++{ ++ PointSizeNone, ++ PointSizeOes, ++ PointSizeExt, ++}; ++ + enum class EsTexbufType + { + TexbufNone, +@@ -46,6 +54,13 @@ enum class EsFbFetchType + FbFetchArm, + }; + ++enum class MultisampleTexStorageType ++{ ++ TexStorageNone, ++ TexStorageCore, ++ TexStorageOes, ++}; ++ + // ogl-only config, so not in VideoConfig.h + struct VideoConfig + { +@@ -60,11 +75,10 @@ struct VideoConfig + bool bSupportsAEP; + bool bSupportsDebug; + bool bSupportsCopySubImage; +- u8 SupportedESPointSize; ++ EsPointSizeType SupportedESPointSize; + EsTexbufType SupportedESTextureBuffer; + bool bSupportsTextureStorage; +- bool bSupports2DTextureStorageMultisample; +- bool bSupports3DTextureStorageMultisample; ++ MultisampleTexStorageType SupportedMultisampleTexStorage; + bool bSupportsConservativeDepth; + bool bSupportsImageLoadStore; + bool bSupportsAniso; +diff -rupN dolphin.orig/Source/Core/VideoBackends/OGL/OGLTexture.cpp dolphin/Source/Core/VideoBackends/OGL/OGLTexture.cpp +--- dolphin.orig/Source/Core/VideoBackends/OGL/OGLTexture.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/OGL/OGLTexture.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -128,12 +128,18 @@ OGLTexture::OGLTexture(const TextureConf + GLenum gl_internal_format = GetGLInternalFormatForTextureFormat(m_config.format, true); + if (tex_config.IsMultisampled()) + { +- if (g_ogl_config.bSupportsTextureStorage) ++ ASSERT(g_ogl_config.bSupportsMSAA); ++ if (g_ogl_config.SupportedMultisampleTexStorage != MultisampleTexStorageType::TexStorageNone) ++ { + glTexStorage3DMultisample(target, tex_config.samples, gl_internal_format, m_config.width, + m_config.height, m_config.layers, GL_FALSE); ++ } + else ++ { ++ ASSERT(!g_ogl_config.bIsES); + glTexImage3DMultisample(target, tex_config.samples, gl_internal_format, m_config.width, + m_config.height, m_config.layers, GL_FALSE); ++ } + } + else if (g_ogl_config.bSupportsTextureStorage) + { +diff -rupN dolphin.orig/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp dolphin/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp +--- dolphin.orig/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/OGL/ProgramShaderCache.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -661,12 +661,13 @@ void ProgramShaderCache::CreateHeader() + std::string SupportedESTextureBuffer; + switch (g_ogl_config.SupportedESPointSize) + { +- case 1: ++ case EsPointSizeType::PointSizeOes: + SupportedESPointSize = "#extension GL_OES_geometry_point_size : enable"; + break; +- case 2: ++ case EsPointSizeType::PointSizeExt: + SupportedESPointSize = "#extension GL_EXT_geometry_point_size : enable"; + break; ++ case EsPointSizeType::PointSizeNone: + default: + SupportedESPointSize = ""; + break; +@@ -718,6 +719,13 @@ void ProgramShaderCache::CreateHeader() + break; + } + ++ // The sampler2DMSArray keyword is reserved in GLSL ES 3.0 and 3.1, but is available in 3.2 and ++ // with GL_OES_texture_storage_multisample_2d_array for 3.1. ++ // See https://bugs.dolphin-emu.org/issues/13198. ++ const bool use_multisample_2d_array_precision = ++ v >= GlslEs320 || ++ g_ogl_config.SupportedMultisampleTexStorage != MultisampleTexStorageType::TexStorageNone; ++ + std::string shader_shuffle_string; + if (g_ogl_config.bSupportsShaderThreadShuffleNV) + { +@@ -762,6 +770,7 @@ void ProgramShaderCache::CreateHeader() + "{}\n" // shader thread shuffle + "{}\n" // derivative control + "{}\n" // query levels ++ "{}\n" // OES multisample texture storage + + // Precision defines for GLSL ES + "{}\n" +@@ -847,12 +856,18 @@ void ProgramShaderCache::CreateHeader() + g_ActiveConfig.backend_info.bSupportsTextureQueryLevels ? + "#extension GL_ARB_texture_query_levels : enable" : + "", ++ // Note: GL_ARB_texture_storage_multisample doesn't have an #extension, as it doesn't ++ // need to change GLSL, but on GLES 3.1 sampler2DMSArray is a reserved keyword unless ++ // the extension is enabled. Thus, we don't need to check TexStorageCore/have an ARB version. ++ g_ogl_config.SupportedMultisampleTexStorage == MultisampleTexStorageType::TexStorageOes ? ++ "#extension GL_OES_texture_storage_multisample_2d_array : enable" : ++ "", + is_glsles ? "precision highp float;" : "", is_glsles ? "precision highp int;" : "", + is_glsles ? "precision highp sampler2DArray;" : "", + (is_glsles && g_ActiveConfig.backend_info.bSupportsPaletteConversion) ? + "precision highp usamplerBuffer;" : + "", +- v > GlslEs300 ? "precision highp sampler2DMSArray;" : "", ++ use_multisample_2d_array_precision ? "precision highp sampler2DMSArray;" : "", + v >= GlslEs310 ? "precision highp image2DArray;" : ""); + } + +diff -rupN dolphin.orig/Source/Core/VideoBackends/Software/SWOGLWindow.cpp dolphin/Source/Core/VideoBackends/Software/SWOGLWindow.cpp +--- dolphin.orig/Source/Core/VideoBackends/Software/SWOGLWindow.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Software/SWOGLWindow.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -32,6 +32,16 @@ bool SWOGLWindow::IsHeadless() const + return m_gl_context->IsHeadless(); + } + ++u32 SWOGLWindow::GetWidth() const ++{ ++ return m_gl_context->GetBackBufferWidth(); ++} ++ ++u32 SWOGLWindow::GetHeight() const ++{ ++ return m_gl_context->GetBackBufferHeight(); ++} ++ + bool SWOGLWindow::Initialize(const WindowSystemInfo& wsi) + { + m_gl_context = GLContext::Create(wsi); +@@ -84,11 +94,17 @@ bool SWOGLWindow::Initialize(const Windo + return true; + } + ++ ++void SWOGLWindow::UpdateDimensions(int window_width, int window_height) ++{ ++ // just updates the render window position and the backbuffer size ++ m_gl_context->UpdateDimensions(window_width, window_height); ++} ++ + void SWOGLWindow::ShowImage(const AbstractTexture* image, + const MathUtil::Rectangle& xfb_region) + { + const SW::SWTexture* sw_image = static_cast(image); +- m_gl_context->Update(); // just updates the render window position and the backbuffer size + + GLsizei glWidth = (GLsizei)m_gl_context->GetBackBufferWidth(); + GLsizei glHeight = (GLsizei)m_gl_context->GetBackBufferHeight(); +diff -rupN dolphin.orig/Source/Core/VideoBackends/Software/SWOGLWindow.h dolphin/Source/Core/VideoBackends/Software/SWOGLWindow.h +--- dolphin.orig/Source/Core/VideoBackends/Software/SWOGLWindow.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Software/SWOGLWindow.h 2024-03-05 04:06:08.240295843 +0000 +@@ -20,6 +20,10 @@ public: + GLContext* GetContext() const { return m_gl_context.get(); } + bool IsHeadless() const; + ++ u32 GetWidth() const; ++ u32 GetHeight() const; ++ void UpdateDimensions(int window_width, int window_height); ++ + // Image to show, will be swapped immediately + void ShowImage(const AbstractTexture* image, const MathUtil::Rectangle& xfb_region); + +diff -rupN dolphin.orig/Source/Core/VideoBackends/Software/SWRenderer.cpp dolphin/Source/Core/VideoBackends/Software/SWRenderer.cpp +--- dolphin.orig/Source/Core/VideoBackends/Software/SWRenderer.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Software/SWRenderer.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -60,17 +60,17 @@ SWRenderer::CreateFramebuffer(AbstractTe + static_cast(depth_attachment)); + } + +-void SWRenderer::BindBackbuffer(const ClearColor& clear_color) +-{ ++//void SWRenderer::BindBackbuffer(const ClearColor& clear_color) ++//{ + // Look for framebuffer resizes +- if (!m_surface_resized.TestAndClear()) +- return; ++ //if (!m_surface_resized.TestAndClear()) ++ //return; + +- GLContext* context = m_window->GetContext(); +- context->Update(); +- m_backbuffer_width = context->GetBackBufferWidth(); +- m_backbuffer_height = context->GetBackBufferHeight(); +-} ++ //GLContext* context = m_window->GetContext(); ++ //context->UpdateDimensions(window_width, window_height); ++ //m_backbuffer_width = context->GetBackBufferWidth(); ++ //m_backbuffer_height = context->GetBackBufferHeight(); ++//} + + class SWShader final : public AbstractShader + { +@@ -108,7 +108,7 @@ std::unique_ptr SWRend + { + return std::make_unique(); + } +- ++ + // Called on the GPU thread + void SWRenderer::RenderXFBToScreen(const MathUtil::Rectangle& target_rc, + const AbstractTexture* source_texture, +@@ -118,6 +118,16 @@ void SWRenderer::RenderXFBToScreen(const + m_window->ShowImage(source_texture, source_rc); + } + ++void SWRenderer::CheckForSurfaceResize() ++{ ++ if (!m_surface_resized.TestAndClear()) ++ return; ++ ++ m_window->UpdateDimensions(m_new_surface_width, m_new_surface_height); ++ m_backbuffer_width = static_cast(m_window->GetWidth()); ++ m_backbuffer_height = static_cast(m_window->GetHeight()); ++} ++ + u32 SWRenderer::AccessEFB(EFBAccessType type, u32 x, u32 y, u32 InputData) + { + u32 value = 0; +diff -rupN dolphin.orig/Source/Core/VideoBackends/Software/SWRenderer.h dolphin/Source/Core/VideoBackends/Software/SWRenderer.h +--- dolphin.orig/Source/Core/VideoBackends/Software/SWRenderer.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Software/SWRenderer.h 2024-03-05 04:06:08.240295843 +0000 +@@ -29,7 +29,7 @@ public: + std::unique_ptr + CreateFramebuffer(AbstractTexture* color_attachment, AbstractTexture* depth_attachment) override; + +- void BindBackbuffer(const ClearColor& clear_color = {}) override; ++ //void BindBackbuffer(const ClearColor& clear_color = {}) override; + + std::unique_ptr CreateShaderFromSource(ShaderStage stage, std::string_view source, + std::string_view name) override; +@@ -64,6 +64,8 @@ protected: + std::unique_ptr CreateBoundingBox() const override; + + private: ++void CheckForSurfaceResize(); ++ + std::unique_ptr m_window; + }; + } // namespace SW +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/CMakeLists.txt dolphin/Source/Core/VideoBackends/Vulkan/CMakeLists.txt +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/CMakeLists.txt 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/CMakeLists.txt 2024-03-05 04:06:08.240295843 +0000 +@@ -35,6 +35,8 @@ add_library(videovulkan + VulkanContext.h + VulkanLoader.cpp + VulkanLoader.h ++ VKScheduler.h ++ VKScheduler.cpp + ) + + target_link_libraries(videovulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/CommandBufferManager.cpp dolphin/Source/Core/VideoBackends/Vulkan/CommandBufferManager.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/CommandBufferManager.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/CommandBufferManager.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -10,24 +10,24 @@ + #include "Common/MsgHandler.h" + #include "Common/Thread.h" + ++#include "VideoBackends/Vulkan/StateTracker.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + + namespace Vulkan + { +-CommandBufferManager::CommandBufferManager(bool use_threaded_submission) +- : m_use_threaded_submission(use_threaded_submission) ++CommandBufferManager::CommandBufferManager() ++ : m_state_tracker(std::make_unique(this)), m_last_present_done(true) + { + } + + CommandBufferManager::~CommandBufferManager() + { + // If the worker thread is enabled, stop and block until it exits. +- if (m_use_threaded_submission) +- { +- WaitForWorkerThreadIdle(); +- m_submit_loop->Stop(); +- m_submit_thread.join(); +- } ++ WaitForSubmitWorkerThreadIdle(); ++ m_submit_loop->Stop(); ++ m_fence_loop->Stop(); ++ m_submit_thread.join(); ++ m_fence_thread.join(); + + DestroyCommandBuffers(); + } +@@ -37,10 +37,13 @@ bool CommandBufferManager::Initialize() + if (!CreateCommandBuffers()) + return false; + +- if (m_use_threaded_submission && !CreateSubmitThread()) ++ if (!CreateFenceThread()) + return false; + +- return true; ++ if (!CreateSubmitThread()) ++ return false; ++ ++ return m_state_tracker->Initialize(); + } + + bool CommandBufferManager::CreateCommandBuffers() +@@ -86,13 +89,6 @@ bool CommandBufferManager::CreateCommand + LOG_VULKAN_ERROR(res, "vkCreateFence failed: "); + return false; + } +- +- res = vkCreateSemaphore(device, &semaphore_create_info, nullptr, &resources.semaphore); +- if (res != VK_SUCCESS) +- { +- LOG_VULKAN_ERROR(res, "vkCreateSemaphore failed: "); +- return false; +- } + } + + res = vkCreateSemaphore(device, &semaphore_create_info, nullptr, &m_present_semaphore); +@@ -125,9 +121,6 @@ void CommandBufferManager::DestroyComman + for (auto& it : resources.cleanup_resources) + it(); + +- if (resources.semaphore != VK_NULL_HANDLE) +- vkDestroySemaphore(device, resources.semaphore, nullptr); +- + if (resources.fence != VK_NULL_HANDLE) + vkDestroyFence(device, resources.fence, nullptr); + } +@@ -215,11 +208,42 @@ VkDescriptorSet CommandBufferManager::Al + return descriptor_set; + } + ++bool CommandBufferManager::CreateFenceThread() ++{ ++ m_fence_loop = std::make_unique(); ++ m_fence_thread = std::thread([this]() { ++ Common::SetCurrentThreadName("Vulkan FenceThread"); ++ m_fence_loop->Run([this]() { ++ PendingFenceCounter fence; ++ { ++ std::lock_guard guard(m_pending_fences_lock); ++ if (m_pending_fences.empty()) ++ { ++ m_fence_condvar.notify_all(); ++ m_fence_loop->AllowSleep(); ++ return; ++ } ++ ++ fence = m_pending_fences.front(); ++ m_pending_fences.pop_front(); ++ } ++ ++ vkWaitForFences(g_vulkan_context->GetDevice(), 1, &fence.fence, true, ~0ul); ++ ++ std::lock_guard guard(m_pending_fences_lock); ++ m_completed_fence_counter.store(fence.counter, std::memory_order_release); ++ m_fence_condvar.notify_all(); ++ }); ++ }); ++ ++ return true; ++} ++ + bool CommandBufferManager::CreateSubmitThread() + { + m_submit_loop = std::make_unique(); + m_submit_thread = std::thread([this]() { +- Common::SetCurrentThreadName("Vulkan CommandBufferManager SubmitThread"); ++ Common::SetCurrentThreadName("Vulkan SubmitThread"); + + m_submit_loop->Run([this]() { + PendingCommandBufferSubmit submit; +@@ -227,9 +251,9 @@ bool CommandBufferManager::CreateSubmitT + std::lock_guard guard(m_pending_submit_lock); + if (m_pending_submits.empty()) + { +- m_submit_loop->AllowSleep(); + m_submit_worker_idle = true; + m_submit_worker_condvar.notify_all(); ++ m_submit_loop->AllowSleep(); + return; + } + +@@ -239,16 +263,12 @@ bool CommandBufferManager::CreateSubmitT + + SubmitCommandBuffer(submit.command_buffer_index, submit.present_swap_chain, + submit.present_image_index); +- CmdBufferResources& resources = m_command_buffers[submit.command_buffer_index]; +- resources.waiting_for_submit.store(false, std::memory_order_release); + ++ std::lock_guard guard(m_pending_submit_lock); ++ if (m_pending_submits.empty()) + { +- std::lock_guard guard(m_pending_submit_lock); +- if (m_pending_submits.empty()) +- { +- m_submit_worker_idle = true; +- m_submit_worker_condvar.notify_all(); +- } ++ m_submit_worker_idle = true; ++ m_submit_worker_condvar.notify_all(); + } + }); + }); +@@ -256,55 +276,28 @@ bool CommandBufferManager::CreateSubmitT + return true; + } + +-void CommandBufferManager::WaitForWorkerThreadIdle() ++void CommandBufferManager::WaitForSubmitWorkerThreadIdle() + { +- if (!m_use_threaded_submission) +- return; +- + std::unique_lock lock{m_pending_submit_lock}; + m_submit_worker_condvar.wait(lock, [&] { return m_submit_worker_idle; }); + } + + void CommandBufferManager::WaitForFenceCounter(u64 fence_counter) + { +- if (m_completed_fence_counter >= fence_counter) ++ if (m_completed_fence_counter.load(std::memory_order_relaxed) >= fence_counter) [[likely]] + return; + +- // Find the first command buffer which covers this counter value. +- u32 index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS; +- while (index != m_current_cmd_buffer) +- { +- if (m_command_buffers[index].fence_counter >= fence_counter) +- break; +- +- index = (index + 1) % NUM_COMMAND_BUFFERS; +- } +- +- ASSERT(index != m_current_cmd_buffer); +- WaitForCommandBufferCompletion(index); ++ std::unique_lock lock{m_pending_fences_lock}; ++ m_fence_condvar.wait(lock, [&] { ++ return m_completed_fence_counter.load(std::memory_order_relaxed) >= fence_counter; ++ }); + } + +-void CommandBufferManager::WaitForCommandBufferCompletion(u32 index) ++void CommandBufferManager::CleanupCompletedCommandBuffers() + { +- CmdBufferResources& resources = m_command_buffers[index]; +- +- // Ensure this command buffer has been submitted. +- if (resources.waiting_for_submit.load(std::memory_order_acquire)) +- { +- WaitForWorkerThreadIdle(); +- ASSERT_MSG(VIDEO, !resources.waiting_for_submit.load(std::memory_order_relaxed), +- "Submit thread is idle but command buffer is still waiting for submission!"); +- } +- +- // Wait for this command buffer to be completed. +- VkResult res = +- vkWaitForFences(g_vulkan_context->GetDevice(), 1, &resources.fence, VK_TRUE, UINT64_MAX); +- if (res != VK_SUCCESS) +- LOG_VULKAN_ERROR(res, "vkWaitForFences failed: "); +- + // Clean up any resources for command buffers between the last known completed buffer and this + // now-completed command buffer. If we use >2 buffers, this may be more than one buffer. +- const u64 now_completed_counter = resources.fence_counter; ++ const u64 now_completed_counter = m_completed_fence_counter.load(std::memory_order_acquire); + u32 cleanup_index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS; + while (cleanup_index != m_current_cmd_buffer) + { +@@ -312,26 +305,22 @@ void CommandBufferManager::WaitForComman + if (cleanup_resources.fence_counter > now_completed_counter) + break; + +- if (cleanup_resources.fence_counter > m_completed_fence_counter) +- { +- for (auto& it : cleanup_resources.cleanup_resources) +- it(); +- cleanup_resources.cleanup_resources.clear(); +- } ++ for (auto& it : cleanup_resources.cleanup_resources) ++ it(); ++ cleanup_resources.cleanup_resources.clear(); + + cleanup_index = (cleanup_index + 1) % NUM_COMMAND_BUFFERS; + } +- +- m_completed_fence_counter = now_completed_counter; + } + +-void CommandBufferManager::SubmitCommandBuffer(bool submit_on_worker_thread, ++void CommandBufferManager::SubmitCommandBuffer(u64 fence_counter, bool submit_on_worker_thread, + bool wait_for_completion, + VkSwapchainKHR present_swap_chain, + uint32_t present_image_index) + { + // End the current command buffer. + CmdBufferResources& resources = GetCurrentCmdBufferResources(); ++ resources.fence_counter = fence_counter; + for (VkCommandBuffer command_buffer : resources.command_buffers) + { + VkResult res = vkEndCommandBuffer(command_buffer); +@@ -344,32 +333,37 @@ void CommandBufferManager::SubmitCommand + } + + // Submitting off-thread? +- if (m_use_threaded_submission && submit_on_worker_thread && !wait_for_completion) ++ if (submit_on_worker_thread && !wait_for_completion) + { +- resources.waiting_for_submit.store(true, std::memory_order_relaxed); + // Push to the pending submit queue. + { + std::lock_guard guard(m_pending_submit_lock); + m_submit_worker_idle = false; + m_pending_submits.push_back({present_swap_chain, present_image_index, m_current_cmd_buffer}); +- } + +- // Wake up the worker thread for a single iteration. +- m_submit_loop->Wakeup(); ++ // Wake up the worker thread for a single iteration. ++ m_submit_loop->Wakeup(); ++ } + } + else + { +- WaitForWorkerThreadIdle(); ++ WaitForSubmitWorkerThreadIdle(); + + // Pass through to normal submission path. + SubmitCommandBuffer(m_current_cmd_buffer, present_swap_chain, present_image_index); + if (wait_for_completion) +- WaitForCommandBufferCompletion(m_current_cmd_buffer); ++ { ++ std::unique_lock lock{m_pending_fences_lock}; ++ m_fence_condvar.wait(lock, [&] { ++ return m_completed_fence_counter.load(std::memory_order_relaxed) >= resources.fence_counter; ++ }); ++ } + } + + if (present_swap_chain != VK_NULL_HANDLE) + { + m_current_frame = (m_current_frame + 1) % NUM_FRAMES_IN_FLIGHT; ++ const u64 now_completed_counter = m_completed_fence_counter.load(std::memory_order_acquire); + + // Wait for all command buffers that used the descriptor pool to finish + u32 cmd_buffer_index = (m_current_cmd_buffer + 1) % NUM_COMMAND_BUFFERS; +@@ -377,9 +371,9 @@ void CommandBufferManager::SubmitCommand + { + CmdBufferResources& cmd_buffer = m_command_buffers[cmd_buffer_index]; + if (cmd_buffer.frame_index == m_current_frame && cmd_buffer.fence_counter != 0 && +- cmd_buffer.fence_counter > m_completed_fence_counter) ++ cmd_buffer.fence_counter > now_completed_counter) + { +- WaitForCommandBufferCompletion(cmd_buffer_index); ++ WaitForFenceCounter(cmd_buffer.fence_counter); + } + cmd_buffer_index = (cmd_buffer_index + 1) % NUM_COMMAND_BUFFERS; + } +@@ -411,6 +405,7 @@ void CommandBufferManager::SubmitCommand + + // Switch to next cmdbuffer. + BeginCommandBuffer(); ++ m_state_tracker->InvalidateCachedState(); + } + + void CommandBufferManager::SubmitCommandBuffer(u32 command_buffer_index, +@@ -455,8 +450,15 @@ void CommandBufferManager::SubmitCommand + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkQueueSubmit failed: "); +- PanicAlertFmt("Failed to submit command buffer: {} ({})", VkResultToString(res), +- static_cast(res)); ++ PanicAlertFmt("Failed to submit command buffer: {} ({}), semaphore used: {}, has present sc {}", ++ VkResultToString(res), static_cast(res), resources.semaphore_used, ++ present_swap_chain != VK_NULL_HANDLE); ++ } ++ ++ { ++ std::lock_guard guard(m_pending_fences_lock); ++ m_pending_fences.push_back({resources.fence, resources.fence_counter}); ++ m_fence_loop->Wakeup(); + } + + // Do we have a swap chain to present? +@@ -471,28 +473,27 @@ void CommandBufferManager::SubmitCommand + &present_swap_chain, + &present_image_index, + nullptr}; +- +- m_last_present_result = vkQueuePresentKHR(g_vulkan_context->GetPresentQueue(), &present_info); +- m_last_present_done.Set(); +- if (m_last_present_result != VK_SUCCESS) ++ res = vkQueuePresentKHR(g_vulkan_context->GetPresentQueue(), &present_info); ++ if (res != VK_SUCCESS) + { + // VK_ERROR_OUT_OF_DATE_KHR is not fatal, just means we need to recreate our swap chain. +- if (m_last_present_result != VK_ERROR_OUT_OF_DATE_KHR && +- m_last_present_result != VK_SUBOPTIMAL_KHR && +- m_last_present_result != VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT) ++ if (res != VK_ERROR_OUT_OF_DATE_KHR && res != VK_SUBOPTIMAL_KHR && ++ res != VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT) + { +- LOG_VULKAN_ERROR(m_last_present_result, "vkQueuePresentKHR failed: "); ++ LOG_VULKAN_ERROR(res, "vkQueuePresentKHR failed: "); + } + + // Don't treat VK_SUBOPTIMAL_KHR as fatal on Android. Android 10+ requires prerotation. + // See https://twitter.com/Themaister/status/1207062674011574273 + #ifdef VK_USE_PLATFORM_ANDROID_KHR +- if (m_last_present_result != VK_SUBOPTIMAL_KHR) ++ if (res != VK_SUBOPTIMAL_KHR) + m_last_present_failed.Set(); + #else + m_last_present_failed.Set(); + #endif + } ++ m_last_present_result.store(res); ++ m_last_present_done.Set(); + } + } + +@@ -503,8 +504,11 @@ void CommandBufferManager::BeginCommandB + CmdBufferResources& resources = m_command_buffers[next_buffer_index]; + + // Wait for the GPU to finish with all resources for this command buffer. +- if (resources.fence_counter > m_completed_fence_counter) +- WaitForCommandBufferCompletion(next_buffer_index); ++ if (resources.fence_counter > m_completed_fence_counter.load(std::memory_order_acquire) && ++ resources.fence_counter != 0) ++ WaitForFenceCounter(resources.fence_counter); ++ ++ CleanupCompletedCommandBuffers(); + + // Reset fence to unsignaled before starting. + VkResult res = vkResetFences(g_vulkan_context->GetDevice(), 1, &resources.fence); +@@ -526,10 +530,9 @@ void CommandBufferManager::BeginCommandB + LOG_VULKAN_ERROR(res, "vkBeginCommandBuffer failed: "); + } + +- // Reset upload command buffer state ++ // Reset command buffer state + resources.init_command_buffer_used = false; + resources.semaphore_used = false; +- resources.fence_counter = m_next_fence_counter++; + resources.frame_index = m_current_frame; + m_current_cmd_buffer = next_buffer_index; + } +@@ -569,6 +572,4 @@ void CommandBufferManager::DeferImageVie + cmd_buffer_resources.cleanup_resources.push_back( + [object]() { vkDestroyImageView(g_vulkan_context->GetDevice(), object, nullptr); }); + } +- +-std::unique_ptr g_command_buffer_mgr; + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/CommandBufferManager.h dolphin/Source/Core/VideoBackends/Vulkan/CommandBufferManager.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/CommandBufferManager.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/CommandBufferManager.h 2024-03-05 04:06:08.240295843 +0000 +@@ -22,10 +22,12 @@ + + namespace Vulkan + { ++class StateTracker; ++ + class CommandBufferManager + { + public: +- explicit CommandBufferManager(bool use_threaded_submission); ++ explicit CommandBufferManager(); + ~CommandBufferManager(); + + bool Initialize(); +@@ -50,39 +52,37 @@ public: + // If the last completed fence counter is greater or equal to N, it means that the work + // associated counter N has been completed by the GPU. The value of N to associate with + // commands can be retreived by calling GetCurrentFenceCounter(). +- u64 GetCompletedFenceCounter() const { return m_completed_fence_counter; } +- +- // Gets the fence that will be signaled when the currently executing command buffer is +- // queued and executed. Do not wait for this fence before the buffer is executed. +- u64 GetCurrentFenceCounter() const ++ // THREAD SAFE ++ u64 GetCompletedFenceCounter() const + { +- auto& resources = m_command_buffers[m_current_cmd_buffer]; +- return resources.fence_counter; ++ return m_completed_fence_counter.load(std::memory_order_acquire); + } + + // Returns the semaphore for the current command buffer, which can be used to ensure the + // swap chain image is ready before the command buffer executes. +- VkSemaphore GetCurrentCommandBufferSemaphore() ++ void SetWaitSemaphoreForCurrentCommandBuffer(VkSemaphore semaphore) + { + auto& resources = m_command_buffers[m_current_cmd_buffer]; + resources.semaphore_used = true; +- return resources.semaphore; ++ resources.semaphore = semaphore; + } + + // Ensure that the worker thread has submitted any previous command buffers and is idle. +- void WaitForWorkerThreadIdle(); ++ void WaitForSubmitWorkerThreadIdle(); + + // Wait for a fence to be completed. + // Also invokes callbacks for completion. ++ // THREAD SAFE + void WaitForFenceCounter(u64 fence_counter); + +- void SubmitCommandBuffer(bool submit_on_worker_thread, bool wait_for_completion, ++ void SubmitCommandBuffer(u64 fence_counter, bool submit_on_worker_thread, ++ bool wait_for_completion, + VkSwapchainKHR present_swap_chain = VK_NULL_HANDLE, + uint32_t present_image_index = 0xFFFFFFFF); + + // Was the last present submitted to the queue a failure? If so, we must recreate our swapchain. + bool CheckLastPresentFail() { return m_last_present_failed.TestAndClear(); } +- VkResult GetLastPresentResult() const { return m_last_present_result; } ++ VkResult GetLastPresentResult() const { return m_last_present_result.load(); } + bool CheckLastPresentDone() { return m_last_present_done.TestAndClear(); } + + // Schedule a vulkan resource for destruction later on. This will occur when the command buffer +@@ -93,17 +93,21 @@ public: + void DeferImageDestruction(VkImage object, VmaAllocation alloc); + void DeferImageViewDestruction(VkImageView object); + ++ StateTracker* GetStateTracker() { return m_state_tracker.get(); } ++ + private: + bool CreateCommandBuffers(); + void DestroyCommandBuffers(); + ++ bool CreateFenceThread(); + bool CreateSubmitThread(); + +- void WaitForCommandBufferCompletion(u32 command_buffer_index); + void SubmitCommandBuffer(u32 command_buffer_index, VkSwapchainKHR present_swap_chain, + u32 present_image_index); + void BeginCommandBuffer(); + ++ void CleanupCompletedCommandBuffers(); ++ + VkDescriptorPool CreateDescriptorPool(u32 descriptor_sizes); + + const u32 DESCRIPTOR_SETS_PER_POOL = 1024; +@@ -118,7 +122,6 @@ private: + u64 fence_counter = 0; + bool init_command_buffer_used = false; + bool semaphore_used = false; +- std::atomic waiting_for_submit{false}; + u32 frame_index = 0; + + std::vector> cleanup_resources; +@@ -137,14 +140,15 @@ private: + return m_command_buffers[m_current_cmd_buffer]; + } + +- u64 m_next_fence_counter = 1; +- u64 m_completed_fence_counter = 0; ++ std::atomic m_completed_fence_counter = 0; + + std::array m_frame_resources; + std::array m_command_buffers; + u32 m_current_frame = 0; + u32 m_current_cmd_buffer = 0; + ++ std::unique_ptr m_state_tracker; ++ + // Threaded command buffer execution + std::thread m_submit_thread; + std::unique_ptr m_submit_loop; +@@ -161,11 +165,20 @@ private: + bool m_submit_worker_idle = true; + Common::Flag m_last_present_failed; + Common::Flag m_last_present_done; +- VkResult m_last_present_result = VK_SUCCESS; +- bool m_use_threaded_submission = false; ++ std::atomic m_last_present_result = VK_SUCCESS; + u32 m_descriptor_set_count = DESCRIPTOR_SETS_PER_POOL; +-}; + +-extern std::unique_ptr g_command_buffer_mgr; ++ // Fence thread ++ std::thread m_fence_thread; ++ std::unique_ptr m_fence_loop; ++ struct PendingFenceCounter ++ { ++ VkFence fence; ++ u64 counter; ++ }; ++ std::deque m_pending_fences; ++ std::mutex m_pending_fences_lock; ++ std::condition_variable m_fence_condvar; ++}; + + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/Constants.h dolphin/Source/Core/VideoBackends/Vulkan/Constants.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/Constants.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/Constants.h 2024-03-05 04:06:08.240295843 +0000 +@@ -12,7 +12,7 @@ + namespace Vulkan + { + // Number of command buffers. +-constexpr size_t NUM_COMMAND_BUFFERS = 8; ++constexpr size_t NUM_COMMAND_BUFFERS = 16; + + // Number of frames in flight, will be used to decide how many descriptor pools are used + constexpr size_t NUM_FRAMES_IN_FLIGHT = 2; +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp dolphin/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -389,6 +389,8 @@ VkSampler ObjectCache::GetSampler(const + VkRenderPass ObjectCache::GetRenderPass(VkFormat color_format, VkFormat depth_format, + u32 multisamples, VkAttachmentLoadOp load_op) + { ++ std::scoped_lock lock(m_render_pass_mutex); ++ + auto key = std::tie(color_format, depth_format, multisamples, load_op); + auto it = m_render_pass_cache.find(key); + if (it != m_render_pass_cache.end()) +@@ -467,6 +469,8 @@ VkRenderPass ObjectCache::GetRenderPass( + + void ObjectCache::DestroyRenderPassCache() + { ++ std::scoped_lock lock(m_render_pass_mutex); ++ + for (auto& it : m_render_pass_cache) + vkDestroyRenderPass(g_vulkan_context->GetDevice(), it.second, nullptr); + m_render_pass_cache.clear(); +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/ObjectCache.h dolphin/Source/Core/VideoBackends/Vulkan/ObjectCache.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/ObjectCache.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/ObjectCache.h 2024-03-05 04:06:08.240295843 +0000 +@@ -7,6 +7,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -102,6 +103,7 @@ private: + std::unique_ptr m_dummy_texture; + + // Render pass cache ++ std::mutex m_render_pass_mutex; + using RenderPassCacheKey = std::tuple; + std::map m_render_pass_cache; + +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp dolphin/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/StagingBuffer.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -8,6 +8,7 @@ + + #include "Common/Assert.h" + ++#include "VKScheduler.h" + #include "VideoBackends/Vulkan/CommandBufferManager.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + #include "VideoCommon/DriverDetails.h" +@@ -26,7 +27,10 @@ StagingBuffer::~StagingBuffer() + if (m_map_pointer) + Unmap(); + +- g_command_buffer_mgr->DeferBufferDestruction(m_buffer, m_alloc); ++ g_scheduler->Record( ++ [c_alloc = m_alloc, c_buffer = m_buffer](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->DeferBufferDestruction(c_buffer, c_alloc); ++ }); + } + + void StagingBuffer::BufferMemoryBarrier(VkCommandBuffer command_buffer, VkBuffer buffer, +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/StateTracker.cpp dolphin/Source/Core/VideoBackends/Vulkan/StateTracker.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/StateTracker.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/StateTracker.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -16,59 +16,93 @@ + + namespace Vulkan + { +-static std::unique_ptr s_state_tracker; +- +-StateTracker::StateTracker() = default; +- +-StateTracker::~StateTracker() = default; ++StateTracker::StateTracker(CommandBufferManager* command_buffer_mgr) ++ : m_command_buffer_mgr(command_buffer_mgr) ++{ ++} + +-StateTracker* StateTracker::GetInstance() ++StateTracker::~StateTracker() + { +- return s_state_tracker.get(); ++ vkDestroyImageView(g_vulkan_context->GetDevice(), m_dummy_view, nullptr); ++ vmaDestroyImage(g_vulkan_context->GetMemoryAllocator(), m_dummy_image, m_dummy_alloc); + } + +-bool StateTracker::CreateInstance() ++bool StateTracker::Initialize() + { +- ASSERT(!s_state_tracker); +- s_state_tracker = std::make_unique(); +- if (!s_state_tracker->Initialize()) ++ // Create a dummy texture which can be used in place of a real binding. ++ VkImageCreateInfo dummy_info; ++ dummy_info.pNext = nullptr; ++ dummy_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; ++ dummy_info.extent = {1, 1, 1}; ++ dummy_info.arrayLayers = 1; ++ dummy_info.mipLevels = 1; ++ dummy_info.flags = 0; ++ dummy_info.imageType = VK_IMAGE_TYPE_2D; ++ dummy_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; ++ dummy_info.samples = VK_SAMPLE_COUNT_1_BIT; ++ dummy_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; ++ dummy_info.format = VK_FORMAT_R8G8B8A8_UNORM; ++ dummy_info.tiling = VK_IMAGE_TILING_OPTIMAL; ++ dummy_info.queueFamilyIndexCount = 0; ++ dummy_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; ++ dummy_info.pQueueFamilyIndices = nullptr; ++ ++ VmaAllocationCreateInfo alloc_create_info = {}; ++ alloc_create_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; ++ alloc_create_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; ++ alloc_create_info.pool = VK_NULL_HANDLE; ++ alloc_create_info.pUserData = nullptr; ++ alloc_create_info.priority = 0.0; ++ alloc_create_info.requiredFlags = 0; ++ alloc_create_info.preferredFlags = 0; ++ ++ VkResult res = vmaCreateImage(g_vulkan_context->GetMemoryAllocator(), &dummy_info, ++ &alloc_create_info, &m_dummy_image, &m_dummy_alloc, nullptr); ++ if (res != VK_SUCCESS) + { +- s_state_tracker.reset(); ++ LOG_VULKAN_ERROR(res, "vmaCreateImage failed: "); + return false; + } +- return true; +-} + +-void StateTracker::DestroyInstance() +-{ +- if (!s_state_tracker) +- return; ++ VkImageViewCreateInfo dummy_view_info; ++ dummy_view_info.pNext = nullptr; ++ dummy_view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; ++ dummy_view_info.flags = 0; ++ dummy_view_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; ++ dummy_view_info.format = VK_FORMAT_R8G8B8A8_UNORM; ++ dummy_view_info.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, ++ VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; ++ dummy_view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; ++ dummy_view_info.image = m_dummy_image; + +- // When the dummy texture is destroyed, it unbinds itself, then references itself. +- // Clear everything out so this doesn't happen. +- for (auto& it : s_state_tracker->m_bindings.samplers) +- it.imageView = VK_NULL_HANDLE; +- s_state_tracker->m_bindings.image_texture.imageView = VK_NULL_HANDLE; +- s_state_tracker->m_dummy_texture.reset(); +- +- s_state_tracker.reset(); +-} +- +-bool StateTracker::Initialize() +-{ +- // Create a dummy texture which can be used in place of a real binding. +- m_dummy_texture = +- VKTexture::Create(TextureConfig(1, 1, 1, 1, 1, AbstractTextureFormat::RGBA8, 0), ""); +- if (!m_dummy_texture) ++ res = vkCreateImageView(g_vulkan_context->GetDevice(), &dummy_view_info, nullptr, &m_dummy_view); ++ if (res != VK_SUCCESS) ++ { ++ LOG_VULKAN_ERROR(res, "vkCreateImageView failed: "); + return false; +- m_dummy_texture->TransitionToLayout(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), +- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ++ } ++ ++ VkImageMemoryBarrier img_barrier; ++ img_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; ++ img_barrier.pNext = nullptr; ++ img_barrier.srcAccessMask = 0; ++ img_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; ++ img_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; ++ img_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; ++ img_barrier.image = m_dummy_image; ++ img_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; ++ img_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; ++ img_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; ++ ++ vkCmdPipelineBarrier(m_command_buffer_mgr->GetCurrentInitCommandBuffer(), ++ VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, ++ nullptr, 0, nullptr, 1, &img_barrier); + + // Initialize all samplers to point by default + for (size_t i = 0; i < NUM_PIXEL_SHADER_SAMPLERS; i++) + { + m_bindings.samplers[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +- m_bindings.samplers[i].imageView = m_dummy_texture->GetView(); ++ m_bindings.samplers[i].imageView = m_dummy_view; + m_bindings.samplers[i].sampler = g_object_cache->GetPointSampler(); + } + +@@ -107,11 +141,17 @@ void StateTracker::SetIndexBuffer(VkBuff + m_dirty_flags |= DIRTY_FLAG_INDEX_BUFFER; + } + +-void StateTracker::SetFramebuffer(VKFramebuffer* framebuffer) ++void StateTracker::SetFramebuffer(VkFramebuffer framebuffer, VkRect2D render_area, ++ VkRenderPass load_render_pass, VkRenderPass clear_render_pass, ++ VkRenderPass discard_render_pass) + { + // Should not be changed within a render pass. + ASSERT(!InRenderPass()); + m_framebuffer = framebuffer; ++ m_framebuffer_render_area = render_area; ++ m_framebuffer_load_render_pass = load_render_pass; ++ m_framebuffer_clear_render_pass = clear_render_pass; ++ m_framebuffer_discard_render_pass = discard_render_pass; + } + + void StateTracker::SetPipeline(const VKPipeline* pipeline) +@@ -232,14 +272,14 @@ void StateTracker::UnbindTexture(VkImage + { + if (it.imageView == view) + { +- it.imageView = m_dummy_texture->GetView(); ++ it.imageView = m_dummy_view; + it.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + } + + if (m_bindings.image_texture.imageView == view) + { +- m_bindings.image_texture.imageView = m_dummy_texture->GetView(); ++ m_bindings.image_texture.imageView = m_dummy_view; + m_bindings.image_texture.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + } +@@ -263,18 +303,18 @@ void StateTracker::BeginRenderPass() + if (InRenderPass()) + return; + +- m_current_render_pass = m_framebuffer->GetLoadRenderPass(); +- m_framebuffer_render_area = m_framebuffer->GetRect(); ++ m_current_render_pass = m_framebuffer_load_render_pass; ++ m_render_area = m_framebuffer_render_area; + + VkRenderPassBeginInfo begin_info = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + nullptr, + m_current_render_pass, +- m_framebuffer->GetFB(), +- m_framebuffer_render_area, ++ m_framebuffer, ++ m_render_area, + 0, + nullptr}; + +- vkCmdBeginRenderPass(g_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, ++ vkCmdBeginRenderPass(m_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, + VK_SUBPASS_CONTENTS_INLINE); + } + +@@ -283,18 +323,18 @@ void StateTracker::BeginDiscardRenderPas + if (InRenderPass()) + return; + +- m_current_render_pass = m_framebuffer->GetDiscardRenderPass(); +- m_framebuffer_render_area = m_framebuffer->GetRect(); ++ m_current_render_pass = m_framebuffer_discard_render_pass; ++ m_render_area = m_framebuffer_render_area; + + VkRenderPassBeginInfo begin_info = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + nullptr, + m_current_render_pass, +- m_framebuffer->GetFB(), +- m_framebuffer_render_area, ++ m_framebuffer, ++ m_render_area, + 0, + nullptr}; + +- vkCmdBeginRenderPass(g_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, ++ vkCmdBeginRenderPass(m_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, + VK_SUBPASS_CONTENTS_INLINE); + } + +@@ -303,7 +343,7 @@ void StateTracker::EndRenderPass() + if (!InRenderPass()) + return; + +- vkCmdEndRenderPass(g_command_buffer_mgr->GetCurrentCommandBuffer()); ++ vkCmdEndRenderPass(m_command_buffer_mgr->GetCurrentCommandBuffer()); + m_current_render_pass = VK_NULL_HANDLE; + } + +@@ -312,18 +352,18 @@ void StateTracker::BeginClearRenderPass( + { + ASSERT(!InRenderPass()); + +- m_current_render_pass = m_framebuffer->GetClearRenderPass(); +- m_framebuffer_render_area = area; ++ m_current_render_pass = m_framebuffer_clear_render_pass; ++ m_render_area = area; + + VkRenderPassBeginInfo begin_info = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + nullptr, + m_current_render_pass, +- m_framebuffer->GetFB(), +- m_framebuffer_render_area, ++ m_framebuffer, ++ m_render_area, + num_clear_values, + clear_values}; + +- vkCmdBeginRenderPass(g_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, ++ vkCmdBeginRenderPass(m_command_buffer_mgr->GetCurrentCommandBuffer(), &begin_info, + VK_SUBPASS_CONTENTS_INLINE); + } + +@@ -352,7 +392,7 @@ bool StateTracker::Bind() + return false; + + // Check the render area if we were in a clear pass. +- if (m_current_render_pass == m_framebuffer->GetClearRenderPass() && !IsViewportWithinRenderArea()) ++ if (m_current_render_pass == m_framebuffer_clear_render_pass && !IsViewportWithinRenderArea()) + EndRenderPass(); + + // Get a new descriptor set if any parts have changed +@@ -363,7 +403,7 @@ bool StateTracker::Bind() + BeginRenderPass(); + + // Re-bind parts of the pipeline +- const VkCommandBuffer command_buffer = g_command_buffer_mgr->GetCurrentCommandBuffer(); ++ const VkCommandBuffer command_buffer = m_command_buffer_mgr->GetCurrentCommandBuffer(); + const bool needs_vertex_buffer = !g_ActiveConfig.backend_info.bSupportsDynamicVertexLoader || + m_pipeline->GetUsage() != AbstractPipelineUsage::GXUber; + if (needs_vertex_buffer && (m_dirty_flags & DIRTY_FLAG_VERTEX_BUFFER)) +@@ -398,7 +438,7 @@ bool StateTracker::BindCompute() + if (InRenderPass()) + EndRenderPass(); + +- const VkCommandBuffer command_buffer = g_command_buffer_mgr->GetCurrentCommandBuffer(); ++ const VkCommandBuffer command_buffer = m_command_buffer_mgr->GetCurrentCommandBuffer(); + if (m_dirty_flags & DIRTY_FLAG_COMPUTE_SHADER) + { + vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_COMPUTE, +@@ -414,10 +454,10 @@ bool StateTracker::IsWithinRenderArea(s3 + { + // Check that the viewport does not lie outside the render area. + // If it does, we need to switch to a normal load/store render pass. +- s32 left = m_framebuffer_render_area.offset.x; +- s32 top = m_framebuffer_render_area.offset.y; +- s32 right = left + static_cast(m_framebuffer_render_area.extent.width); +- s32 bottom = top + static_cast(m_framebuffer_render_area.extent.height); ++ s32 left = m_render_area.offset.x; ++ s32 top = m_render_area.offset.y; ++ s32 right = left + static_cast(m_render_area.extent.width); ++ s32 bottom = top + static_cast(m_render_area.extent.height); + s32 test_left = x; + s32 test_top = y; + s32 test_right = test_left + static_cast(width); +@@ -434,7 +474,7 @@ bool StateTracker::IsViewportWithinRende + + void StateTracker::EndClearRenderPass() + { +- if (m_current_render_pass != m_framebuffer->GetClearRenderPass()) ++ if (m_current_render_pass != m_framebuffer_clear_render_pass) + return; + + // End clear render pass. Bind() will call BeginRenderPass() which +@@ -463,7 +503,7 @@ void StateTracker::UpdateGXDescriptorSet + + if (m_dirty_flags & DIRTY_FLAG_GX_UBOS || m_gx_descriptor_sets[0] == VK_NULL_HANDLE) + { +- m_gx_descriptor_sets[0] = g_command_buffer_mgr->AllocateDescriptorSet( ++ m_gx_descriptor_sets[0] = m_command_buffer_mgr->AllocateDescriptorSet( + g_object_cache->GetDescriptorSetLayout(DESCRIPTOR_SET_LAYOUT_STANDARD_UNIFORM_BUFFERS)); + + for (size_t i = 0; i < NUM_UBO_DESCRIPTOR_SET_BINDINGS; i++) +@@ -490,7 +530,7 @@ void StateTracker::UpdateGXDescriptorSet + + if (m_dirty_flags & DIRTY_FLAG_GX_SAMPLERS || m_gx_descriptor_sets[1] == VK_NULL_HANDLE) + { +- m_gx_descriptor_sets[1] = g_command_buffer_mgr->AllocateDescriptorSet( ++ m_gx_descriptor_sets[1] = m_command_buffer_mgr->AllocateDescriptorSet( + g_object_cache->GetDescriptorSetLayout(DESCRIPTOR_SET_LAYOUT_STANDARD_SAMPLERS)); + + writes[num_writes++] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, +@@ -516,7 +556,7 @@ void StateTracker::UpdateGXDescriptorSet + (m_dirty_flags & DIRTY_FLAG_GX_SSBO || m_gx_descriptor_sets[2] == VK_NULL_HANDLE)) + { + m_gx_descriptor_sets[2] = +- g_command_buffer_mgr->AllocateDescriptorSet(g_object_cache->GetDescriptorSetLayout( ++ m_command_buffer_mgr->AllocateDescriptorSet(g_object_cache->GetDescriptorSetLayout( + DESCRIPTOR_SET_LAYOUT_STANDARD_SHADER_STORAGE_BUFFERS)); + + writes[num_writes++] = { +@@ -546,7 +586,7 @@ void StateTracker::UpdateGXDescriptorSet + + if (m_dirty_flags & DIRTY_FLAG_DESCRIPTOR_SETS) + { +- vkCmdBindDescriptorSets(g_command_buffer_mgr->GetCurrentCommandBuffer(), ++ vkCmdBindDescriptorSets(m_command_buffer_mgr->GetCurrentCommandBuffer(), + VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline->GetVkPipelineLayout(), 0, + needs_ssbo ? NUM_GX_DESCRIPTOR_SETS : (NUM_GX_DESCRIPTOR_SETS - 1), + m_gx_descriptor_sets.data(), +@@ -558,7 +598,7 @@ void StateTracker::UpdateGXDescriptorSet + else if (m_dirty_flags & DIRTY_FLAG_GX_UBO_OFFSETS) + { + vkCmdBindDescriptorSets( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, ++ m_command_buffer_mgr->GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, + m_pipeline->GetVkPipelineLayout(), 0, 1, m_gx_descriptor_sets.data(), + needs_gs_ubo ? NUM_UBO_DESCRIPTOR_SET_BINDINGS : (NUM_UBO_DESCRIPTOR_SET_BINDINGS - 1), + m_bindings.gx_ubo_offsets.data()); +@@ -575,7 +615,7 @@ void StateTracker::UpdateUtilityDescript + // Allocate descriptor sets. + if (m_dirty_flags & DIRTY_FLAG_UTILITY_UBO || m_utility_descriptor_sets[0] == VK_NULL_HANDLE) + { +- m_utility_descriptor_sets[0] = g_command_buffer_mgr->AllocateDescriptorSet( ++ m_utility_descriptor_sets[0] = m_command_buffer_mgr->AllocateDescriptorSet( + g_object_cache->GetDescriptorSetLayout(DESCRIPTOR_SET_LAYOUT_UTILITY_UNIFORM_BUFFER)); + + dswrites[writes++] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, +@@ -594,7 +634,7 @@ void StateTracker::UpdateUtilityDescript + + if (m_dirty_flags & DIRTY_FLAG_UTILITY_BINDINGS || m_utility_descriptor_sets[1] == VK_NULL_HANDLE) + { +- m_utility_descriptor_sets[1] = g_command_buffer_mgr->AllocateDescriptorSet( ++ m_utility_descriptor_sets[1] = m_command_buffer_mgr->AllocateDescriptorSet( + g_object_cache->GetDescriptorSetLayout(DESCRIPTOR_SET_LAYOUT_UTILITY_SAMPLERS)); + + dswrites[writes++] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, +@@ -626,7 +666,7 @@ void StateTracker::UpdateUtilityDescript + + if (m_dirty_flags & DIRTY_FLAG_DESCRIPTOR_SETS) + { +- vkCmdBindDescriptorSets(g_command_buffer_mgr->GetCurrentCommandBuffer(), ++ vkCmdBindDescriptorSets(m_command_buffer_mgr->GetCurrentCommandBuffer(), + VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline->GetVkPipelineLayout(), 0, + NUM_UTILITY_DESCRIPTOR_SETS, m_utility_descriptor_sets.data(), 1, + &m_bindings.utility_ubo_offset); +@@ -634,7 +674,7 @@ void StateTracker::UpdateUtilityDescript + } + else if (m_dirty_flags & DIRTY_FLAG_UTILITY_UBO_OFFSET) + { +- vkCmdBindDescriptorSets(g_command_buffer_mgr->GetCurrentCommandBuffer(), ++ vkCmdBindDescriptorSets(m_command_buffer_mgr->GetCurrentCommandBuffer(), + VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline->GetVkPipelineLayout(), 0, + 1, m_utility_descriptor_sets.data(), 1, &m_bindings.utility_ubo_offset); + m_dirty_flags &= ~(DIRTY_FLAG_DESCRIPTOR_SETS | DIRTY_FLAG_UTILITY_UBO_OFFSET); +@@ -649,7 +689,7 @@ void StateTracker::UpdateComputeDescript + // Allocate descriptor sets. + if (m_dirty_flags & DIRTY_FLAG_COMPUTE_BINDINGS) + { +- m_compute_descriptor_set = g_command_buffer_mgr->AllocateDescriptorSet( ++ m_compute_descriptor_set = m_command_buffer_mgr->AllocateDescriptorSet( + g_object_cache->GetDescriptorSetLayout(DESCRIPTOR_SET_LAYOUT_COMPUTE)); + dswrites[0] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + nullptr, +@@ -700,7 +740,7 @@ void StateTracker::UpdateComputeDescript + + if (m_dirty_flags & DIRTY_FLAG_COMPUTE_DESCRIPTOR_SET) + { +- vkCmdBindDescriptorSets(g_command_buffer_mgr->GetCurrentCommandBuffer(), ++ vkCmdBindDescriptorSets(m_command_buffer_mgr->GetCurrentCommandBuffer(), + VK_PIPELINE_BIND_POINT_COMPUTE, + g_object_cache->GetPipelineLayout(PIPELINE_LAYOUT_COMPUTE), 0, 1, + &m_compute_descriptor_set, 1, &m_bindings.utility_ubo_offset); +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/StateTracker.h dolphin/Source/Core/VideoBackends/Vulkan/StateTracker.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/StateTracker.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/StateTracker.h 2024-03-05 04:06:08.240295843 +0000 +@@ -13,28 +13,24 @@ + + namespace Vulkan + { +-class VKFramebuffer; + class VKShader; + class VKPipeline; +-class VKTexture; +-class StreamBuffer; +-class VertexFormat; ++class CommandBufferManager; + + class StateTracker + { + public: +- StateTracker(); ++ StateTracker(CommandBufferManager* command_buffer_mgr); + ~StateTracker(); + +- static StateTracker* GetInstance(); +- static bool CreateInstance(); +- static void DestroyInstance(); ++ bool Initialize(); + +- VKFramebuffer* GetFramebuffer() const { return m_framebuffer; } + const VKPipeline* GetPipeline() const { return m_pipeline; } + void SetVertexBuffer(VkBuffer buffer, VkDeviceSize offset, u32 size); + void SetIndexBuffer(VkBuffer buffer, VkDeviceSize offset, VkIndexType type); +- void SetFramebuffer(VKFramebuffer* framebuffer); ++ void SetFramebuffer(VkFramebuffer framebuffer, VkRect2D render_area, ++ VkRenderPass load_render_pass, VkRenderPass clear_render_pass, ++ VkRenderPass discard_render_pass); + void SetPipeline(const VKPipeline* pipeline); + void SetComputeShader(const VKShader* shader); + void SetGXUniformBuffer(u32 index, VkBuffer buffer, u32 offset, u32 size); +@@ -110,8 +106,6 @@ private: + DIRTY_FLAG_UTILITY_BINDINGS | DIRTY_FLAG_COMPUTE_BINDINGS + }; + +- bool Initialize(); +- + // Check that the specified viewport is within the render area. + // If not, ends the render pass if it is a clear render pass. + bool IsViewportWithinRenderArea() const; +@@ -121,6 +115,8 @@ private: + void UpdateUtilityDescriptorSet(); + void UpdateComputeDescriptorSet(); + ++ CommandBufferManager* m_command_buffer_mgr; ++ + // Which bindings/state has to be updated before the next draw. + u32 m_dirty_flags = 0; + +@@ -157,10 +153,17 @@ private: + VkRect2D m_scissor = {{0, 0}, {1, 1}}; + + // uniform buffers +- std::unique_ptr m_dummy_texture; ++ VkImage m_dummy_image; ++ VkImageView m_dummy_view; ++ VmaAllocation m_dummy_alloc; + +- VKFramebuffer* m_framebuffer = nullptr; +- VkRenderPass m_current_render_pass = VK_NULL_HANDLE; ++ VkFramebuffer m_framebuffer = VK_NULL_HANDLE; + VkRect2D m_framebuffer_render_area = {}; ++ VkRenderPass m_framebuffer_load_render_pass = VK_NULL_HANDLE; ++ VkRenderPass m_framebuffer_discard_render_pass = VK_NULL_HANDLE; ++ VkRenderPass m_framebuffer_clear_render_pass = VK_NULL_HANDLE; ++ ++ VkRenderPass m_current_render_pass = VK_NULL_HANDLE; ++ VkRect2D m_render_area = {}; + }; + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKBoundingBox.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKBoundingBox.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKBoundingBox.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKBoundingBox.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -12,6 +12,7 @@ + #include "VideoBackends/Vulkan/StagingBuffer.h" + #include "VideoBackends/Vulkan/StateTracker.h" + #include "VideoBackends/Vulkan/VKRenderer.h" ++#include "VideoBackends/Vulkan/VKScheduler.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + + namespace Vulkan +@@ -33,36 +34,43 @@ bool VKBoundingBox::Initialize() + return false; + + // Bind bounding box to state tracker +- StateTracker::GetInstance()->SetSSBO(m_gpu_buffer, 0, BUFFER_SIZE); ++ g_scheduler->Record([c_gpu_buffer = m_gpu_buffer](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetSSBO(c_gpu_buffer, 0, BUFFER_SIZE); ++ }); + return true; + } + + std::vector VKBoundingBox::Read(u32 index, u32 length) + { +- // Can't be done within a render pass. +- StateTracker::GetInstance()->EndRenderPass(); +- +- // Ensure all writes are completed to the GPU buffer prior to the transfer. +- StagingBuffer::BufferMemoryBarrier( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, +- VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0, +- BUFFER_SIZE, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); +- m_readback_buffer->PrepareForGPUWrite(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_ACCESS_TRANSFER_WRITE_BIT, +- VK_PIPELINE_STAGE_TRANSFER_BIT); +- +- // Copy from GPU -> readback buffer. +- VkBufferCopy region = {0, 0, BUFFER_SIZE}; +- vkCmdCopyBuffer(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, +- m_readback_buffer->GetBuffer(), 1, ®ion); +- +- // Restore GPU buffer access. +- StagingBuffer::BufferMemoryBarrier( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, VK_ACCESS_TRANSFER_READ_BIT, +- VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0, BUFFER_SIZE, +- VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); +- m_readback_buffer->FlushGPUCache(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); ++ std::vector values(length); ++ // We can just take a reference here, we'll sync immediately afterwards ++ g_scheduler->Record([&](CommandBufferManager* command_buffer_mgr) { ++ // Can't be done within a render pass. ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ ++ // Ensure all writes are completed to the GPU buffer prior to the transfer. ++ StagingBuffer::BufferMemoryBarrier( ++ command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, ++ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0, ++ BUFFER_SIZE, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); ++ ++ m_readback_buffer->PrepareForGPUWrite(command_buffer_mgr->GetCurrentCommandBuffer(), ++ VK_ACCESS_TRANSFER_WRITE_BIT, ++ VK_PIPELINE_STAGE_TRANSFER_BIT); ++ ++ // Copy from GPU -> readback buffer. ++ VkBufferCopy region = {0, 0, BUFFER_SIZE}; ++ vkCmdCopyBuffer(command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, ++ m_readback_buffer->GetBuffer(), 1, ®ion); ++ ++ // Restore GPU buffer access. ++ StagingBuffer::BufferMemoryBarrier( ++ command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, VK_ACCESS_TRANSFER_READ_BIT, ++ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0, BUFFER_SIZE, ++ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); ++ m_readback_buffer->FlushGPUCache(command_buffer_mgr->GetCurrentCommandBuffer(), ++ VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); ++ }); + + // Wait until these commands complete. + Renderer::GetInstance()->ExecuteCommandBuffer(false, true); +@@ -71,34 +79,37 @@ std::vector VKBoundingBox::Rea + m_readback_buffer->InvalidateCPUCache(); + + // Read out the values and return +- std::vector values(length); + m_readback_buffer->Read(index * sizeof(BBoxType), values.data(), length * sizeof(BBoxType), + false); ++ + return values; + } + + void VKBoundingBox::Write(u32 index, const std::vector& values) + { +- // We can't issue vkCmdUpdateBuffer within a render pass. +- // However, the writes must be serialized, so we can't put it in the init buffer. +- StateTracker::GetInstance()->EndRenderPass(); +- +- // Ensure GPU buffer is in a state where it can be transferred to. +- StagingBuffer::BufferMemoryBarrier( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, +- VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, +- BUFFER_SIZE, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); +- +- // Write the values to the GPU buffer +- vkCmdUpdateBuffer(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, +- index * sizeof(BBoxType), values.size() * sizeof(BBoxType), +- reinterpret_cast(values.data())); +- +- // Restore fragment shader access to the buffer. +- StagingBuffer::BufferMemoryBarrier( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), m_gpu_buffer, VK_ACCESS_TRANSFER_WRITE_BIT, +- VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0, BUFFER_SIZE, +- VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); ++ g_scheduler->Record([c_gpu_buffer = m_gpu_buffer, c_values = values, ++ c_index = index](CommandBufferManager* command_buffer_mgr) { ++ // We can't issue vkCmdUpdateBuffer within a render pass. ++ // However, the writes must be serialized, so we can't put it in the init buffer. ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ ++ // Ensure GPU buffer is in a state where it can be transferred to. ++ StagingBuffer::BufferMemoryBarrier( ++ command_buffer_mgr->GetCurrentCommandBuffer(), c_gpu_buffer, ++ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, ++ BUFFER_SIZE, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); ++ ++ // Write the values to the GPU buffer ++ vkCmdUpdateBuffer(command_buffer_mgr->GetCurrentCommandBuffer(), c_gpu_buffer, ++ c_index * sizeof(BBoxType), c_values.size() * sizeof(BBoxType), ++ reinterpret_cast(c_values.data())); ++ ++ // Restore fragment shader access to the buffer. ++ StagingBuffer::BufferMemoryBarrier( ++ command_buffer_mgr->GetCurrentCommandBuffer(), c_gpu_buffer, VK_ACCESS_TRANSFER_WRITE_BIT, ++ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0, BUFFER_SIZE, ++ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); ++ }); + } + + bool VKBoundingBox::CreateGPUBuffer() +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKMain.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKMain.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKMain.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKMain.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -18,6 +18,7 @@ + #include "VideoBackends/Vulkan/VKVertexManager.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + ++#include "VKScheduler.h" + #include "VideoCommon/FramebufferManager.h" + #include "VideoCommon/TextureCacheBase.h" + #include "VideoCommon/VideoBackendBase.h" +@@ -196,14 +197,7 @@ bool VideoBackend::Initialize(const Wind + // With the backend information populated, we can now initialize videocommon. + InitializeShared(); + +- // Create command buffers. We do this separately because the other classes depend on it. +- g_command_buffer_mgr = std::make_unique(g_Config.bBackendMultithreading); +- if (!g_command_buffer_mgr->Initialize()) +- { +- PanicAlertFmt("Failed to create Vulkan command buffers"); +- Shutdown(); +- return false; +- } ++ g_scheduler = std::make_unique(); + + // Remaining classes are also dependent on object cache. + g_object_cache = std::make_unique(); +@@ -214,6 +208,14 @@ bool VideoBackend::Initialize(const Wind + return false; + } + ++ // Has to be initialized after the object cache ++ if (!g_scheduler->Initialize()) ++ { ++ PanicAlertFmt("Failed to initialize Vulkan scheduler."); ++ Shutdown(); ++ return false; ++ } ++ + // Create swap chain. This has to be done early so that the target size is correct for auto-scale. + std::unique_ptr swap_chain; + if (surface != VK_NULL_HANDLE) +@@ -227,13 +229,6 @@ bool VideoBackend::Initialize(const Wind + } + } + +- if (!StateTracker::CreateInstance()) +- { +- PanicAlertFmt("Failed to create state tracker"); +- Shutdown(); +- return false; +- } +- + // Create main wrapper instances. + g_renderer = std::make_unique(std::move(swap_chain), wsi.render_surface_scale); + g_vertex_manager = std::make_unique(); +@@ -257,6 +252,9 @@ bool VideoBackend::Initialize(const Wind + + void VideoBackend::Shutdown() + { ++ if (g_scheduler) ++ g_scheduler->SyncWorker(); ++ + if (g_vulkan_context) + vkDeviceWaitIdle(g_vulkan_context->GetDevice()); + +@@ -269,6 +267,9 @@ void VideoBackend::Shutdown() + if (g_renderer) + g_renderer->Shutdown(); + ++ if (g_scheduler) ++ g_scheduler->Shutdown(); ++ + g_perf_query.reset(); + g_texture_cache.reset(); + g_framebuffer_manager.reset(); +@@ -276,8 +277,7 @@ void VideoBackend::Shutdown() + g_vertex_manager.reset(); + g_renderer.reset(); + g_object_cache.reset(); +- StateTracker::DestroyInstance(); +- g_command_buffer_mgr.reset(); ++ g_scheduler.reset(); + g_vulkan_context.reset(); + ShutdownShared(); + UnloadVulkanLibrary(); +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKPerfQuery.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKPerfQuery.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKPerfQuery.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKPerfQuery.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -11,6 +11,7 @@ + #include "Common/Logging/Log.h" + #include "Common/MsgHandler.h" + ++#include "VKScheduler.h" + #include "VideoBackends/Vulkan/CommandBufferManager.h" + #include "VideoBackends/Vulkan/StateTracker.h" + #include "VideoBackends/Vulkan/VKRenderer.h" +@@ -49,9 +50,11 @@ void PerfQuery::EnableQuery(PerfQueryGro + if (query_count > m_query_buffer.size() / 2) + PartialFlush(query_count == PERF_QUERY_BUFFER_SIZE); + +- // Ensure command buffer is ready to go before beginning the query, that way we don't submit +- // a buffer with open queries. +- StateTracker::GetInstance()->Bind(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ // Ensure command buffer is ready to go before beginning the query, that way we don't submit ++ // a buffer with open queries. ++ command_buffer_mgr->GetStateTracker()->Bind(); ++ }); + + if (group == PQG_ZCOMP_ZCOMPLOC || group == PQG_ZCOMP) + { +@@ -59,15 +62,16 @@ void PerfQuery::EnableQuery(PerfQueryGro + DEBUG_ASSERT(!entry.has_value); + entry.has_value = true; + entry.query_group = group; +- +- // Use precise queries if supported, otherwise boolean (which will be incorrect). +- VkQueryControlFlags flags = +- g_vulkan_context->SupportsPreciseOcclusionQueries() ? VK_QUERY_CONTROL_PRECISE_BIT : 0; +- +- // Ensure the query starts within a render pass. +- StateTracker::GetInstance()->BeginRenderPass(); +- vkCmdBeginQuery(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_query_pool, m_query_next_pos, +- flags); ++ g_scheduler->Record([c_query_pool = m_query_pool, ++ c_pos = m_query_next_pos](CommandBufferManager* command_buffer_mgr) { ++ // Use precise queries if supported, otherwise boolean (which will be incorrect). ++ VkQueryControlFlags flags = ++ g_vulkan_context->SupportsPreciseOcclusionQueries() ? VK_QUERY_CONTROL_PRECISE_BIT : 0; ++ ++ // Ensure the query starts within a render pass. ++ command_buffer_mgr->GetStateTracker()->BeginRenderPass(); ++ vkCmdBeginQuery(command_buffer_mgr->GetCurrentCommandBuffer(), c_query_pool, c_pos, flags); ++ }); + } + } + +@@ -75,10 +79,13 @@ void PerfQuery::DisableQuery(PerfQueryGr + { + if (group == PQG_ZCOMP_ZCOMPLOC || group == PQG_ZCOMP) + { +- vkCmdEndQuery(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_query_pool, m_query_next_pos); +- ActiveQuery& entry = m_query_buffer[m_query_next_pos]; +- entry.fence_counter = g_command_buffer_mgr->GetCurrentFenceCounter(); ++ g_scheduler->Record([c_query_pool = m_query_pool, c_pos = m_query_next_pos + ++ ](CommandBufferManager* command_buffer_mgr) { ++ vkCmdEndQuery(command_buffer_mgr->GetCurrentCommandBuffer(), c_query_pool, c_pos); ++ }); ++ ActiveQuery& entry = m_query_buffer[m_query_next_pos]; ++ entry.fence_counter = g_scheduler->GetCurrentFenceCounter(); + m_query_next_pos = (m_query_next_pos + 1) % PERF_QUERY_BUFFER_SIZE; + m_query_count.fetch_add(1, std::memory_order_relaxed); + } +@@ -93,10 +100,11 @@ void PerfQuery::ResetQuery() + m_results[i].store(0, std::memory_order_relaxed); + + // Reset entire query pool, ensuring all queries are ready to write to. +- StateTracker::GetInstance()->EndRenderPass(); +- vkCmdResetQueryPool(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_query_pool, 0, +- PERF_QUERY_BUFFER_SIZE); +- ++ g_scheduler->Record([c_query_pool = m_query_pool](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ vkCmdResetQueryPool(command_buffer_mgr->GetCurrentCommandBuffer(), c_query_pool, 0, ++ PERF_QUERY_BUFFER_SIZE); ++ }); + std::memset(m_query_buffer.data(), 0, sizeof(ActiveQuery) * m_query_buffer.size()); + } + +@@ -160,7 +168,7 @@ bool PerfQuery::CreateQueryPool() + + void PerfQuery::ReadbackQueries() + { +- const u64 completed_fence_counter = g_command_buffer_mgr->GetCompletedFenceCounter(); ++ const u64 completed_fence_counter = g_scheduler->GetCompletedFenceCounter(); + + // Need to save these since ProcessResults will modify them. + const u32 outstanding_queries = m_query_count.load(std::memory_order_relaxed); +@@ -201,9 +209,12 @@ void PerfQuery::ReadbackQueries(u32 quer + if (res != VK_SUCCESS) + LOG_VULKAN_ERROR(res, "vkGetQueryPoolResults failed: "); + +- StateTracker::GetInstance()->EndRenderPass(); +- vkCmdResetQueryPool(g_command_buffer_mgr->GetCurrentCommandBuffer(), m_query_pool, +- m_query_readback_pos, query_count); ++ g_scheduler->Record([c_query_pool = m_query_pool, c_query_readback_pos = m_query_readback_pos, ++ query_count](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ vkCmdResetQueryPool(command_buffer_mgr->GetCurrentCommandBuffer(), c_query_pool, ++ c_query_readback_pos, query_count); ++ }); + + // Remove pending queries. + for (u32 i = 0; i < query_count; i++) +@@ -231,8 +242,8 @@ void PerfQuery::ReadbackQueries(u32 quer + void PerfQuery::PartialFlush(bool blocking) + { + // Submit a command buffer in the background if the front query is not bound to one. +- if (blocking || m_query_buffer[m_query_readback_pos].fence_counter == +- g_command_buffer_mgr->GetCurrentFenceCounter()) ++ if (blocking || ++ m_query_buffer[m_query_readback_pos].fence_counter == g_scheduler->GetCurrentFenceCounter()) + { + Renderer::GetInstance()->ExecuteCommandBuffer(true, blocking); + } +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKRenderer.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKRenderer.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKRenderer.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKRenderer.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -31,6 +31,7 @@ + #include "VideoBackends/Vulkan/VKVertexFormat.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + ++#include "VKScheduler.h" + #include "VideoCommon/DriverDetails.h" + #include "VideoCommon/FramebufferManager.h" + #include "VideoCommon/RenderState.h" +@@ -124,7 +125,10 @@ std::unique_ptr Ren + + void Renderer::SetPipeline(const AbstractPipeline* pipeline) + { +- StateTracker::GetInstance()->SetPipeline(static_cast(pipeline)); ++ g_scheduler->Record([c_pipeline = static_cast(pipeline)]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetPipeline(c_pipeline); ++ }); + } + + std::unique_ptr Renderer::CreateBoundingBox() const +@@ -138,17 +142,6 @@ void Renderer::ClearScreen(const MathUti + g_framebuffer_manager->FlushEFBPokes(); + g_framebuffer_manager->FlagPeekCacheAsOutOfDate(); + +- // Native -> EFB coordinates +- MathUtil::Rectangle target_rc = Renderer::ConvertEFBRectangle(rc); +- +- // Size we pass this size to vkBeginRenderPass, it has to be clamped to the framebuffer +- // dimensions. The other backends just silently ignore this case. +- target_rc.ClampUL(0, 0, m_target_width, m_target_height); +- +- VkRect2D target_vk_rc = { +- {target_rc.left, target_rc.top}, +- {static_cast(target_rc.GetWidth()), static_cast(target_rc.GetHeight())}}; +- + // Determine whether the EFB has an alpha channel. If it doesn't, we can clear the alpha + // channel to 0xFF. This hopefully allows us to use the fast path in most cases. + if (bpmem.zcontrol.pixel_format == PixelFormat::RGB565_Z16 || +@@ -161,84 +154,98 @@ void Renderer::ClearScreen(const MathUti + color &= 0x00FFFFFF; + } + +- // Convert RGBA8 -> floating-point values. +- VkClearValue clear_color_value = {}; +- VkClearValue clear_depth_value = {}; +- clear_color_value.color.float32[0] = static_cast((color >> 16) & 0xFF) / 255.0f; +- clear_color_value.color.float32[1] = static_cast((color >> 8) & 0xFF) / 255.0f; +- clear_color_value.color.float32[2] = static_cast((color >> 0) & 0xFF) / 255.0f; +- clear_color_value.color.float32[3] = static_cast((color >> 24) & 0xFF) / 255.0f; +- clear_depth_value.depthStencil.depth = static_cast(z & 0xFFFFFF) / 16777216.0f; +- if (!g_ActiveConfig.backend_info.bSupportsReversedDepthRange) +- clear_depth_value.depthStencil.depth = 1.0f - clear_depth_value.depthStencil.depth; +- +- // If we're not in a render pass (start of the frame), we can use a clear render pass +- // to discard the data, rather than loading and then clearing. +- bool use_clear_attachments = (color_enable && alpha_enable) || z_enable; +- bool use_clear_render_pass = +- !StateTracker::GetInstance()->InRenderPass() && color_enable && alpha_enable && z_enable; +- +- // The NVIDIA Vulkan driver causes the GPU to lock up, or throw exceptions if MSAA is enabled, +- // a non-full clear rect is specified, and a clear loadop or vkCmdClearAttachments is used. +- if (g_ActiveConfig.iMultisamples > 1 && +- DriverDetails::HasBug(DriverDetails::BUG_BROKEN_MSAA_CLEAR)) +- { +- use_clear_render_pass = false; +- use_clear_attachments = false; +- } +- +- // This path cannot be used if the driver implementation doesn't guarantee pixels with no drawn +- // geometry in "this" renderpass won't be cleared +- if (DriverDetails::HasBug(DriverDetails::BUG_BROKEN_CLEAR_LOADOP_RENDERPASS)) +- use_clear_render_pass = false; +- +- // Fastest path: Use a render pass to clear the buffers. +- if (use_clear_render_pass) +- { +- const std::array clear_values = {{clear_color_value, clear_depth_value}}; +- StateTracker::GetInstance()->BeginClearRenderPass(target_vk_rc, clear_values.data(), +- static_cast(clear_values.size())); +- return; +- } ++ // Native -> EFB coordinates ++ MathUtil::Rectangle target_rc = Renderer::ConvertEFBRectangle(rc); ++ ++ // Size we pass this size to vkBeginRenderPass, it has to be clamped to the framebuffer ++ // dimensions. The other backends just silently ignore this case. ++ target_rc.ClampUL(0, 0, m_target_width, m_target_height); ++ ++ g_scheduler->Record([color, z, color_enable, alpha_enable, z_enable, ++ target_rc](CommandBufferManager* command_buffer_mgr) mutable { ++ VkRect2D target_vk_rc = {{target_rc.left, target_rc.top}, ++ {static_cast(target_rc.GetWidth()), ++ static_cast(target_rc.GetHeight())}}; ++ ++ // Convert RGBA8 -> floating-point values. ++ VkClearValue clear_color_value = {}; ++ VkClearValue clear_depth_value = {}; ++ clear_color_value.color.float32[0] = static_cast((color >> 16) & 0xFF) / 255.0f; ++ clear_color_value.color.float32[1] = static_cast((color >> 8) & 0xFF) / 255.0f; ++ clear_color_value.color.float32[2] = static_cast((color >> 0) & 0xFF) / 255.0f; ++ clear_color_value.color.float32[3] = static_cast((color >> 24) & 0xFF) / 255.0f; ++ clear_depth_value.depthStencil.depth = static_cast(z & 0xFFFFFF) / 16777216.0f; ++ if (!g_ActiveConfig.backend_info.bSupportsReversedDepthRange) ++ clear_depth_value.depthStencil.depth = 1.0f - clear_depth_value.depthStencil.depth; ++ ++ // If we're not in a render pass (start of the frame), we can use a clear render pass ++ // to discard the data, rather than loading and then clearing. ++ bool use_clear_attachments = (color_enable && alpha_enable) || z_enable; ++ bool use_clear_render_pass = !command_buffer_mgr->GetStateTracker()->InRenderPass() && ++ color_enable && alpha_enable && z_enable; ++ ++ // The NVIDIA Vulkan driver causes the GPU to lock up, or throw exceptions if MSAA is enabled, ++ // a non-full clear rect is specified, and a clear loadop or vkCmdClearAttachments is used. ++ if (g_ActiveConfig.iMultisamples > 1 && ++ DriverDetails::HasBug(DriverDetails::BUG_BROKEN_MSAA_CLEAR)) ++ { ++ use_clear_render_pass = false; ++ use_clear_attachments = false; ++ } ++ ++ // This path cannot be used if the driver implementation doesn't guarantee pixels with no drawn ++ // geometry in "this" renderpass won't be cleared ++ if (DriverDetails::HasBug(DriverDetails::BUG_BROKEN_CLEAR_LOADOP_RENDERPASS)) ++ use_clear_render_pass = false; + +- // Fast path: Use vkCmdClearAttachments to clear the buffers within a render path +- // We can't use this when preserving alpha but clearing color. +- if (use_clear_attachments) +- { +- VkClearAttachment clear_attachments[2]; +- uint32_t num_clear_attachments = 0; +- if (color_enable && alpha_enable) +- { +- clear_attachments[num_clear_attachments].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +- clear_attachments[num_clear_attachments].colorAttachment = 0; +- clear_attachments[num_clear_attachments].clearValue = clear_color_value; +- num_clear_attachments++; +- color_enable = false; +- alpha_enable = false; +- } +- if (z_enable) +- { +- clear_attachments[num_clear_attachments].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; +- clear_attachments[num_clear_attachments].colorAttachment = 0; +- clear_attachments[num_clear_attachments].clearValue = clear_depth_value; +- num_clear_attachments++; +- z_enable = false; +- } +- if (num_clear_attachments > 0) +- { +- VkClearRect vk_rect = {target_vk_rc, 0, g_framebuffer_manager->GetEFBLayers()}; +- if (!StateTracker::GetInstance()->IsWithinRenderArea( +- target_vk_rc.offset.x, target_vk_rc.offset.y, target_vk_rc.extent.width, +- target_vk_rc.extent.height)) ++ // Fastest path: Use a render pass to clear the buffers. ++ if (use_clear_render_pass) ++ { ++ const std::array clear_values = {{clear_color_value, clear_depth_value}}; ++ command_buffer_mgr->GetStateTracker()->BeginClearRenderPass( ++ target_vk_rc, clear_values.data(), static_cast(clear_values.size())); ++ return; ++ } ++ ++ // Fast path: Use vkCmdClearAttachments to clear the buffers within a render path ++ // We can't use this when preserving alpha but clearing color. ++ if (use_clear_attachments) ++ { ++ VkClearAttachment clear_attachments[2]; ++ uint32_t num_clear_attachments = 0; ++ if (color_enable && alpha_enable) ++ { ++ clear_attachments[num_clear_attachments].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ++ clear_attachments[num_clear_attachments].colorAttachment = 0; ++ clear_attachments[num_clear_attachments].clearValue = clear_color_value; ++ num_clear_attachments++; ++ color_enable = false; ++ alpha_enable = false; ++ } ++ if (z_enable) + { +- StateTracker::GetInstance()->EndClearRenderPass(); ++ clear_attachments[num_clear_attachments].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; ++ clear_attachments[num_clear_attachments].colorAttachment = 0; ++ clear_attachments[num_clear_attachments].clearValue = clear_depth_value; ++ num_clear_attachments++; ++ z_enable = false; + } +- StateTracker::GetInstance()->BeginRenderPass(); ++ if (num_clear_attachments > 0) ++ { ++ VkClearRect vk_rect = {target_vk_rc, 0, g_framebuffer_manager->GetEFBLayers()}; ++ if (!command_buffer_mgr->GetStateTracker()->IsWithinRenderArea( ++ target_vk_rc.offset.x, target_vk_rc.offset.y, target_vk_rc.extent.width, ++ target_vk_rc.extent.height)) ++ { ++ command_buffer_mgr->GetStateTracker()->EndClearRenderPass(); ++ } ++ command_buffer_mgr->GetStateTracker()->BeginRenderPass(); + +- vkCmdClearAttachments(g_command_buffer_mgr->GetCurrentCommandBuffer(), num_clear_attachments, +- clear_attachments, 1, &vk_rect); ++ vkCmdClearAttachments(command_buffer_mgr->GetCurrentCommandBuffer(), num_clear_attachments, ++ clear_attachments, 1, &vk_rect); ++ } + } +- } ++ }); + + // Anything left over for the slow path? + if (!color_enable && !alpha_enable && !z_enable) +@@ -259,10 +266,11 @@ void Renderer::WaitForGPUIdle() + + void Renderer::BindBackbuffer(const ClearColor& clear_color) + { +- StateTracker::GetInstance()->EndRenderPass(); +- +- if (!g_command_buffer_mgr->CheckLastPresentDone()) +- g_command_buffer_mgr->WaitForWorkerThreadIdle(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); ++ if (!g_scheduler->CheckLastPresentDone()) ++ g_scheduler->SynchronizeSubmissionThread(); + + // Handle host window resizes. + CheckForSurfaceChange(); +@@ -276,9 +284,21 @@ void Renderer::BindBackbuffer(const Clea + m_swap_chain->SetNextFullscreenState(m_swap_chain->GetCurrentFullscreenState()); + } + +- const bool present_fail = g_command_buffer_mgr->CheckLastPresentFail(); +- VkResult res = present_fail ? g_command_buffer_mgr->GetLastPresentResult() : +- m_swap_chain->AcquireNextImage(); ++ VkSemaphore semaphore = VK_NULL_HANDLE; ++ VkResult res; ++ const bool present_fail = g_scheduler->CheckLastPresentFail(); ++ if (!present_fail) ++ { ++ semaphore = m_swap_chain->GetNextSemaphore(); ++ g_scheduler->Record([c_semaphore = semaphore](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->SetWaitSemaphoreForCurrentCommandBuffer(c_semaphore); ++ }); ++ res = m_swap_chain->AcquireNextImage(semaphore); ++ } ++ else ++ { ++ res = g_scheduler->GetLastPresentResult(); ++ } + + if (res == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT && + !m_swap_chain->GetCurrentFullscreenState()) +@@ -289,8 +309,13 @@ void Renderer::BindBackbuffer(const Clea + res = VK_SUCCESS; + if (present_fail) + { ++ if (semaphore == VK_NULL_HANDLE) ++ { ++ semaphore = m_swap_chain->GetNextSemaphore(); ++ } ++ + // We still need to acquire an image. +- res = m_swap_chain->AcquireNextImage(); ++ res = m_swap_chain->AcquireNextImage(semaphore); + } + } + +@@ -309,7 +334,7 @@ void Renderer::BindBackbuffer(const Clea + else if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR) + { + INFO_LOG_FMT(VIDEO, "Resizing swap chain due to suboptimal/out-of-date"); +- m_swap_chain->ResizeSwapChain(); ++ m_swap_chain->ResizeSwapChain(m_backbuffer_width, m_backbuffer_height); + } + else + { +@@ -318,7 +343,12 @@ void Renderer::BindBackbuffer(const Clea + m_swap_chain->RecreateSwapChain(); + } + +- res = m_swap_chain->AcquireNextImage(); ++ semaphore = m_swap_chain->GetNextSemaphore(); ++ g_scheduler->Record([c_semaphore = semaphore](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->SetWaitSemaphoreForCurrentCommandBuffer(c_semaphore); ++ }); ++ ++ res = m_swap_chain->AcquireNextImage(semaphore); + if (res != VK_SUCCESS) + PanicAlertFmt("Failed to grab image from swap chain: {:#010X} {}", static_cast(res), + VkResultToString(res)); +@@ -328,8 +358,7 @@ void Renderer::BindBackbuffer(const Clea + // color attachment ready for writing. These transitions must occur outside + // a render pass, unless the render pass declares a self-dependency. + m_swap_chain->GetCurrentTexture()->OverrideImageLayout(VK_IMAGE_LAYOUT_UNDEFINED); +- m_swap_chain->GetCurrentTexture()->TransitionToLayout( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ++ m_swap_chain->GetCurrentTexture()->TransitionToLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + SetAndClearFramebuffer(m_swap_chain->GetCurrentFramebuffer(), + ClearColor{{0.0f, 0.0f, 0.0f, 1.0f}}); + } +@@ -337,22 +366,20 @@ void Renderer::BindBackbuffer(const Clea + void Renderer::PresentBackbuffer() + { + // End drawing to backbuffer +- StateTracker::GetInstance()->EndRenderPass(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); + + // Transition the backbuffer to PRESENT_SRC to ensure all commands drawing + // to it have finished before present. +- m_swap_chain->GetCurrentTexture()->TransitionToLayout( +- g_command_buffer_mgr->GetCurrentCommandBuffer(), VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); ++ m_swap_chain->GetCurrentTexture()->TransitionToLayout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + + // Submit the current command buffer, signaling rendering finished semaphore when it's done + // Because this final command buffer is rendering to the swap chain, we need to wait for + // the available semaphore to be signaled before executing the buffer. This final submission + // can happen off-thread in the background while we're preparing the next frame. +- g_command_buffer_mgr->SubmitCommandBuffer(true, false, m_swap_chain->GetSwapChain(), +- m_swap_chain->GetCurrentImageIndex()); +- +- // New cmdbuffer, so invalidate state. +- StateTracker::GetInstance()->InvalidateCachedState(); ++ g_scheduler->SubmitCommandBuffer(true, false, m_swap_chain->GetSwapChain(), ++ m_swap_chain->GetCurrentImageIndex()); + } + + void Renderer::SetFullscreen(bool enable_fullscreen) +@@ -370,11 +397,7 @@ bool Renderer::IsFullscreen() const + + void Renderer::ExecuteCommandBuffer(bool submit_off_thread, bool wait_for_completion) + { +- StateTracker::GetInstance()->EndRenderPass(); +- +- g_command_buffer_mgr->SubmitCommandBuffer(submit_off_thread, wait_for_completion); +- +- StateTracker::GetInstance()->InvalidateCachedState(); ++ g_scheduler->SubmitCommandBuffer(submit_off_thread, wait_for_completion); + } + + void Renderer::CheckForSurfaceChange() +@@ -382,15 +405,20 @@ void Renderer::CheckForSurfaceChange() + if (!m_surface_changed.TestAndClear() || !m_swap_chain) + return; + ++ g_scheduler->SyncWorker(); ++ + // Submit the current draws up until rendering the XFB. + ExecuteCommandBuffer(false, true); + + // Clear the present failed flag, since we don't want to resize after recreating. +- g_command_buffer_mgr->CheckLastPresentFail(); ++ g_scheduler->CheckLastPresentFail(); + + // Recreate the surface. If this fails we're in trouble. +- if (!m_swap_chain->RecreateSurface(m_new_surface_handle)) ++ if (!m_swap_chain->RecreateSurface(m_new_surface_handle, m_new_surface_width, ++ m_new_surface_height)) ++ { + PanicAlertFmt("Failed to recreate Vulkan surface. Cannot continue."); ++ } + m_new_surface_handle = nullptr; + + // Handle case where the dimensions are now different. +@@ -402,6 +430,8 @@ void Renderer::CheckForSurfaceResize() + if (!m_surface_resized.TestAndClear()) + return; + ++ g_scheduler->SyncWorker(); ++ + // If we don't have a surface, how can we resize the swap chain? + // CheckForSurfaceChange should handle this case. + if (!m_swap_chain) +@@ -414,17 +444,21 @@ void Renderer::CheckForSurfaceResize() + ExecuteCommandBuffer(false, true); + + // Clear the present failed flag, since we don't want to resize after recreating. +- g_command_buffer_mgr->CheckLastPresentFail(); ++ g_scheduler->CheckLastPresentFail(); + + // Resize the swap chain. +- m_swap_chain->RecreateSwapChain(); ++ m_swap_chain->ResizeSwapChain(m_new_surface_width, m_new_surface_height); + OnSwapChainResized(); + } + + void Renderer::OnConfigChanged(u32 bits) + { + if (bits & CONFIG_CHANGE_BIT_HOST_CONFIG) +- g_object_cache->ReloadPipelineCache(); ++ { ++ g_scheduler->Record([](CommandBufferManager* command_buffer_manager) { ++ g_object_cache->ReloadPipelineCache(); ++ }); ++ } + + // For vsync, we need to change the present mode, which means recreating the swap chain. + if (m_swap_chain && bits & CONFIG_CHANGE_BIT_VSYNC) +@@ -456,22 +490,32 @@ void Renderer::OnSwapChainResized() + + void Renderer::BindFramebuffer(VKFramebuffer* fb) + { +- StateTracker::GetInstance()->EndRenderPass(); ++ g_scheduler->Record([fb](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); + +- // Shouldn't be bound as a texture. +- if (fb->GetColorAttachment()) +- { +- StateTracker::GetInstance()->UnbindTexture( +- static_cast(fb->GetColorAttachment())->GetView()); +- } +- if (fb->GetDepthAttachment()) +- { +- StateTracker::GetInstance()->UnbindTexture( +- static_cast(fb->GetDepthAttachment())->GetView()); +- } ++ // Shouldn't be bound as a texture. ++ if (fb->GetColorAttachment()) ++ { ++ command_buffer_mgr->GetStateTracker()->UnbindTexture( ++ static_cast(fb->GetColorAttachment())->GetView()); ++ } ++ if (fb->GetDepthAttachment()) ++ { ++ command_buffer_mgr->GetStateTracker()->UnbindTexture( ++ static_cast(fb->GetDepthAttachment())->GetView()); ++ } ++ }); + + fb->TransitionForRender(); +- StateTracker::GetInstance()->SetFramebuffer(fb); ++ ++ g_scheduler->Record([c_framebuffer = fb->GetFB(), c_rect = fb->GetRect(), ++ c_load_render_pass = fb->GetLoadRenderPass(), ++ c_clear_render_pass = fb->GetClearRenderPass(), ++ c_discard_render_pass = ++ fb->GetDiscardRenderPass()](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetFramebuffer( ++ c_framebuffer, c_rect, c_load_render_pass, c_clear_render_pass, c_discard_render_pass); ++ }); + m_current_framebuffer = fb; + } + +@@ -494,7 +538,9 @@ void Renderer::SetAndDiscardFramebuffer( + + // If we're discarding, begin the discard pass, then switch to a load pass. + // This way if the command buffer is flushed, we don't start another discard pass. +- StateTracker::GetInstance()->BeginDiscardRenderPass(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->BeginDiscardRenderPass(); ++ }); + } + + void Renderer::SetAndClearFramebuffer(AbstractFramebuffer* framebuffer, +@@ -503,22 +549,25 @@ void Renderer::SetAndClearFramebuffer(Ab + VKFramebuffer* vkfb = static_cast(framebuffer); + BindFramebuffer(vkfb); + +- std::array clear_values; +- u32 num_clear_values = 0; +- if (vkfb->GetColorFormat() != AbstractTextureFormat::Undefined) +- { +- std::memcpy(clear_values[num_clear_values].color.float32, color_value.data(), +- sizeof(clear_values[num_clear_values].color.float32)); +- num_clear_values++; +- } +- if (vkfb->GetDepthFormat() != AbstractTextureFormat::Undefined) +- { +- clear_values[num_clear_values].depthStencil.depth = depth_value; +- clear_values[num_clear_values].depthStencil.stencil = 0; +- num_clear_values++; +- } +- StateTracker::GetInstance()->BeginClearRenderPass(vkfb->GetRect(), clear_values.data(), +- num_clear_values); ++ g_scheduler->Record( ++ [vkfb, c_color_value = color_value, depth_value](CommandBufferManager* command_buffer_mgr) { ++ std::array clear_values; ++ u32 num_clear_values = 0; ++ if (vkfb->GetColorFormat() != AbstractTextureFormat::Undefined) ++ { ++ std::memcpy(clear_values[num_clear_values].color.float32, c_color_value.data(), ++ sizeof(clear_values[num_clear_values].color.float32)); ++ num_clear_values++; ++ } ++ if (vkfb->GetDepthFormat() != AbstractTextureFormat::Undefined) ++ { ++ clear_values[num_clear_values].depthStencil.depth = depth_value; ++ clear_values[num_clear_values].depthStencil.stencil = 0; ++ num_clear_values++; ++ } ++ command_buffer_mgr->GetStateTracker()->BeginClearRenderPass( ++ vkfb->GetRect(), clear_values.data(), num_clear_values); ++ }); + } + + void Renderer::SetTexture(u32 index, const AbstractTexture* texture) +@@ -526,25 +575,31 @@ void Renderer::SetTexture(u32 index, con + // Texture should always be in SHADER_READ_ONLY layout prior to use. + // This is so we don't need to transition during render passes. + const VKTexture* tex = static_cast(texture); ++ + if (tex) + { + if (tex->GetLayout() != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + { +- if (StateTracker::GetInstance()->InRenderPass()) +- { +- WARN_LOG_FMT(VIDEO, "Transitioning image in render pass in Renderer::SetTexture()"); +- StateTracker::GetInstance()->EndRenderPass(); +- } ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ if (command_buffer_mgr->GetStateTracker()->InRenderPass()) ++ { ++ WARN_LOG_FMT(VIDEO, "Transitioning image in render pass in Renderer::SetTexture()"); ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ } ++ }); + +- tex->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ++ tex->TransitionToLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + +- StateTracker::GetInstance()->SetTexture(index, tex->GetView()); ++ g_scheduler->Record([c_view = tex->GetView(), index](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetTexture(index, c_view); ++ }); + } + else + { +- StateTracker::GetInstance()->SetTexture(0, VK_NULL_HANDLE); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetTexture(0, VK_NULL_HANDLE); ++ }); + } + } + +@@ -554,15 +609,17 @@ void Renderer::SetSamplerState(u32 index + if (m_sampler_states[index] == state) + return; + +- // Look up new state and replace in state tracker. +- VkSampler sampler = g_object_cache->GetSampler(state); +- if (sampler == VK_NULL_HANDLE) +- { +- ERROR_LOG_FMT(VIDEO, "Failed to create sampler"); +- sampler = g_object_cache->GetPointSampler(); +- } ++ g_scheduler->Record([index, c_sampler_state = state](CommandBufferManager* command_buffer_mgr) { ++ // Look up new state and replace in state tracker. ++ VkSampler sampler = g_object_cache->GetSampler(c_sampler_state); ++ if (sampler == VK_NULL_HANDLE) ++ { ++ ERROR_LOG_FMT(VIDEO, "Failed to create sampler"); ++ sampler = g_object_cache->GetPointSampler(); ++ } + +- StateTracker::GetInstance()->SetSampler(index, sampler); ++ command_buffer_mgr->GetStateTracker()->SetSampler(index, sampler); ++ }); + m_sampler_states[index] = state; + } + +@@ -571,87 +628,116 @@ void Renderer::SetComputeImageTexture(Ab + VKTexture* vk_texture = static_cast(texture); + if (vk_texture) + { +- StateTracker::GetInstance()->EndRenderPass(); +- StateTracker::GetInstance()->SetImageTexture(vk_texture->GetView()); +- vk_texture->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- read ? (write ? VKTexture::ComputeImageLayout::ReadWrite : ++ g_scheduler->Record([c_view = vk_texture->GetView()](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ command_buffer_mgr->GetStateTracker()->SetImageTexture(c_view); ++ }); ++ ++ vk_texture->TransitionToLayout(read ? (write ? VKTexture::ComputeImageLayout::ReadWrite : + VKTexture::ComputeImageLayout::ReadOnly) : + VKTexture::ComputeImageLayout::WriteOnly); + } + else + { +- StateTracker::GetInstance()->SetImageTexture(VK_NULL_HANDLE); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetImageTexture(VK_NULL_HANDLE); ++ }); + } + } + + void Renderer::UnbindTexture(const AbstractTexture* texture) + { +- StateTracker::GetInstance()->UnbindTexture(static_cast(texture)->GetView()); ++ g_scheduler->Record([c_view = static_cast(texture)->GetView()]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->UnbindTexture(c_view); ++ }); + } + + void Renderer::ResetSamplerStates() + { +- // Invalidate all sampler states, next draw will re-initialize them. + for (u32 i = 0; i < m_sampler_states.size(); i++) + { + m_sampler_states[i] = RenderState::GetPointSamplerState(); +- StateTracker::GetInstance()->SetSampler(i, g_object_cache->GetPointSampler()); + } + +- // Invalidate all sampler objects (some will be unused now). +- g_object_cache->ClearSamplerCache(); ++ g_scheduler->Record( ++ [c_sampler_count = m_sampler_states.size()](CommandBufferManager* command_buffer_mgr) { ++ // Invalidate all sampler states, next draw will re-initialize them. ++ for (u32 i = 0; i < c_sampler_count; i++) ++ { ++ command_buffer_mgr->GetStateTracker()->SetSampler(i, g_object_cache->GetPointSampler()); ++ } ++ ++ // Invalidate all sampler objects (some will be unused now). ++ g_object_cache->ClearSamplerCache(); ++ }); + } + + void Renderer::SetScissorRect(const MathUtil::Rectangle& rc) + { +- VkRect2D scissor = {{rc.left, rc.top}, +- {static_cast(rc.GetWidth()), static_cast(rc.GetHeight())}}; +- +- // See Vulkan spec for vkCmdSetScissor: +- // The x and y members of offset must be greater than or equal to 0. +- if (scissor.offset.x < 0) +- { +- scissor.extent.width -= -scissor.offset.x; +- scissor.offset.x = 0; +- } +- if (scissor.offset.y < 0) +- { +- scissor.extent.height -= -scissor.offset.y; +- scissor.offset.y = 0; +- } +- StateTracker::GetInstance()->SetScissor(scissor); ++ g_scheduler->Record([c_rc = rc](CommandBufferManager* command_buffer_mgr) { ++ VkRect2D scissor = {{c_rc.left, c_rc.top}, ++ {static_cast(c_rc.GetWidth()), static_cast(c_rc.GetHeight())}}; ++ ++ // See Vulkan spec for vkCmdSetScissor: ++ // The x and y members of offset must be greater than or equal to 0. ++ if (scissor.offset.x < 0) ++ { ++ scissor.extent.width -= -scissor.offset.x; ++ scissor.offset.x = 0; ++ } ++ if (scissor.offset.y < 0) ++ { ++ scissor.extent.height -= -scissor.offset.y; ++ scissor.offset.y = 0; ++ } ++ command_buffer_mgr->GetStateTracker()->SetScissor(scissor); ++ }); + } + + void Renderer::SetViewport(float x, float y, float width, float height, float near_depth, + float far_depth) + { + VkViewport viewport = {x, y, width, height, near_depth, far_depth}; +- StateTracker::GetInstance()->SetViewport(viewport); ++ g_scheduler->Record([viewport](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetViewport(viewport); ++ }); + } + + void Renderer::Draw(u32 base_vertex, u32 num_vertices) + { +- if (!StateTracker::GetInstance()->Bind()) +- return; ++ g_scheduler->Record([base_vertex, num_vertices](CommandBufferManager* command_buffer_mgr) { ++ if (!command_buffer_mgr->GetStateTracker()->Bind()) ++ return; + +- vkCmdDraw(g_command_buffer_mgr->GetCurrentCommandBuffer(), num_vertices, 1, base_vertex, 0); ++ vkCmdDraw(command_buffer_mgr->GetCurrentCommandBuffer(), num_vertices, 1, base_vertex, 0); ++ }); + } + + void Renderer::DrawIndexed(u32 base_index, u32 num_indices, u32 base_vertex) + { +- if (!StateTracker::GetInstance()->Bind()) +- return; +- +- vkCmdDrawIndexed(g_command_buffer_mgr->GetCurrentCommandBuffer(), num_indices, 1, base_index, +- base_vertex, 0); ++ g_scheduler->Record( ++ [base_vertex, num_indices, base_index](CommandBufferManager* command_buffer_mgr) { ++ if (!command_buffer_mgr->GetStateTracker()->Bind()) ++ return; ++ ++ vkCmdDrawIndexed(command_buffer_mgr->GetCurrentCommandBuffer(), num_indices, 1, base_index, ++ base_vertex, 0); ++ }); + } + + void Renderer::DispatchComputeShader(const AbstractShader* shader, u32 groupsize_x, u32 groupsize_y, + u32 groupsize_z, u32 groups_x, u32 groups_y, u32 groups_z) + { +- StateTracker::GetInstance()->SetComputeShader(static_cast(shader)); +- if (StateTracker::GetInstance()->BindCompute()) +- vkCmdDispatch(g_command_buffer_mgr->GetCurrentCommandBuffer(), groups_x, groups_y, groups_z); ++ g_scheduler->Record([groups_x, groups_y, groups_z, ++ shader](CommandBufferManager* command_buffer_mgr) { ++ if (!command_buffer_mgr->GetStateTracker()->Bind()) ++ return; ++ ++ command_buffer_mgr->GetStateTracker()->SetComputeShader(static_cast(shader)); ++ if (command_buffer_mgr->GetStateTracker()->BindCompute()) ++ vkCmdDispatch(command_buffer_mgr->GetCurrentCommandBuffer(), groups_x, groups_y, groups_z); ++ }); + } + + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKScheduler.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKScheduler.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKScheduler.cpp 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKScheduler.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,155 @@ ++// Copyright 2022 Dolphin Emulator Project ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++#include "VKScheduler.h" ++#include ++#include "Common/Thread.h" ++#include "StateTracker.h" ++ ++namespace Vulkan ++{ ++Scheduler::Scheduler() ++ : m_commandBufferManager(std::make_unique()), ++ m_submit_loop(std::make_unique()) ++{ ++ AcquireNewChunk(); ++ ++ m_worker = std::thread([this]() { ++ Common::SetCurrentThreadName("Vulkan CS Thread"); ++ WorkerThread(); ++ }); ++} ++ ++Scheduler::~Scheduler() ++{ ++ m_submit_loop->Stop(); ++ m_worker.join(); ++} ++ ++bool Scheduler::Initialize() ++{ ++ return m_commandBufferManager->Initialize(); ++} ++ ++void Scheduler::CommandChunk::ExecuteAll(CommandBufferManager* cmdbuf) ++{ ++ auto command = first; ++ while (command != nullptr) ++ { ++ auto next = command->GetNext(); ++ command->Execute(cmdbuf); ++ command->~Command(); ++ command = next; ++ } ++ command_offset = 0; ++ first = nullptr; ++ last = nullptr; ++} ++ ++void Scheduler::AcquireNewChunk() ++{ ++ std::scoped_lock lock{m_reserve_mutex}; ++ if (m_chunk_reserve.empty()) ++ { ++ m_chunk = std::make_unique(); ++ return; ++ } ++ m_chunk = std::move(m_chunk_reserve.back()); ++ m_chunk_reserve.pop_back(); ++} ++ ++void Scheduler::Flush() ++{ ++ if (m_chunk->Empty()) ++ return; ++ ++ { ++ std::scoped_lock lock{m_work_mutex}; ++ m_worker_idle = false; ++ m_work_queue.push(std::move(m_chunk)); ++ m_submit_loop->Wakeup(); ++ } ++ AcquireNewChunk(); ++} ++ ++void Scheduler::SyncWorker() ++{ ++ Flush(); ++ std::unique_lock lock{m_work_mutex}; ++ m_idle_condvar.wait(lock, [this] { return m_worker_idle; }); ++} ++ ++void Scheduler::WorkerThread() ++{ ++ m_submit_loop->Run([this]() { ++ std::unique_ptr work; ++ { ++ std::scoped_lock lock{m_work_mutex}; ++ if (m_work_queue.empty()) ++ { ++ m_worker_idle = true; ++ m_idle_condvar.notify_all(); ++ m_submit_loop->AllowSleep(); ++ return; ++ } ++ work = std::move(m_work_queue.front()); ++ m_work_queue.pop(); ++ } ++ ++ work->ExecuteAll(m_commandBufferManager.get()); ++ { ++ std::scoped_lock reserve_lock{m_reserve_mutex}; ++ m_chunk_reserve.push_back(std::move(work)); ++ } ++ { ++ std::scoped_lock lock{m_work_mutex}; ++ if (m_work_queue.empty()) ++ { ++ m_worker_idle = true; ++ m_idle_condvar.notify_all(); ++ } ++ } ++ }); ++} ++ ++void Scheduler::Shutdown() ++{ ++ SyncWorker(); ++ SynchronizeSubmissionThread(); ++} ++ ++void Scheduler::SynchronizeSubmissionThread() ++{ ++ SyncWorker(); ++ m_commandBufferManager->WaitForSubmitWorkerThreadIdle(); ++} ++ ++void Scheduler::WaitForFenceCounter(u64 counter) ++{ ++ if (m_commandBufferManager->GetCompletedFenceCounter() >= counter) ++ return; ++ ++ SyncWorker(); ++ m_commandBufferManager->WaitForFenceCounter(counter); ++} ++ ++void Scheduler::SubmitCommandBuffer(bool submit_on_worker_thread, bool wait_for_completion, ++ VkSwapchainKHR present_swap_chain, uint32_t present_image_index) ++{ ++ const u64 fence_counter = ++m_current_fence_counter; ++ Record([fence_counter, submit_on_worker_thread, wait_for_completion, present_swap_chain, ++ present_image_index](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ command_buffer_mgr->SubmitCommandBuffer(fence_counter, submit_on_worker_thread, ++ wait_for_completion, present_swap_chain, ++ present_image_index); ++ }); ++ ++ if (wait_for_completion) [[unlikely]] ++ g_scheduler->WaitForFenceCounter(fence_counter); ++ else ++ Flush(); ++} ++ ++std::unique_ptr g_scheduler; ++} // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKScheduler.h dolphin/Source/Core/VideoBackends/Vulkan/VKScheduler.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKScheduler.h 1970-01-01 00:00:00.000000000 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKScheduler.h 2024-03-05 04:06:08.240295843 +0000 +@@ -0,0 +1,160 @@ ++// Copyright 2022 Dolphin Emulator Project ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++#pragma once ++ ++#include ++#include ++ ++#include "CommandBufferManager.h" ++#include "Common/Align.h" ++ ++namespace Vulkan ++{ ++class Scheduler ++{ ++ class Command ++ { ++ public: ++ virtual ~Command() = default; ++ ++ virtual void Execute(CommandBufferManager* cmdbuf) = 0; ++ ++ Command* GetNext() const { return next; } ++ ++ void SetNext(Command* next_) { next = next_; } ++ ++ private: ++ Command* next = nullptr; ++ }; ++ ++ template ++ class alignas(16) TypedCommand final : public Command ++ { ++ public: ++ explicit TypedCommand(T&& command_) : command{std::move(command_)} {} ++ ++ ~TypedCommand() override = default; ++ ++ TypedCommand(TypedCommand&&) = delete; ++ ++ TypedCommand& operator=(TypedCommand&&) = delete; ++ ++ void Execute(CommandBufferManager* cmdbuf) override { command(cmdbuf); } ++ ++ private: ++ T command; ++ }; ++ ++ class CommandChunk final ++ { ++ public: ++ void ExecuteAll(CommandBufferManager* cmdbuf); ++ ++ template ++ bool Record(T& command) ++ { ++ using FuncType = TypedCommand; ++ static_assert(sizeof(FuncType) < sizeof(data), "Lambda is too large"); ++ ++ command_offset = Common::AlignUp(command_offset, alignof(FuncType)); ++ if (command_offset > sizeof(data) - sizeof(FuncType)) [[unlikely]] ++ { ++ return false; ++ } ++ Command* const current_last = last; ++ last = new (data.data() + command_offset) FuncType(std::move(command)); ++ ++ if (current_last) [[likely]] ++ { ++ current_last->SetNext(last); ++ } ++ else ++ { ++ first = last; ++ } ++ command_offset += sizeof(FuncType); ++ return true; ++ } ++ ++ bool Empty() const { return command_offset == 0; } ++ ++ private: ++ Command* first = nullptr; ++ Command* last = nullptr; ++ ++ size_t command_offset = 0; ++ alignas(64) std::array data{}; ++ }; ++ ++public: ++ Scheduler(); ++ ~Scheduler(); ++ ++ bool Initialize(); ++ ++ void Flush(); ++ void SyncWorker(); ++ ++ void Shutdown(); ++ ++ template ++ void Record(T&& command) ++ { ++#ifdef VULKAN_DISABLE_THREADING ++ command(m_commandBufferManager.get()); ++ return; ++#endif ++ ++ if (m_chunk->Record(command)) [[likely]] ++ return; ++ ++ Flush(); ++ (void)m_chunk->Record(command); ++ } ++ ++ uint64_t GetCompletedFenceCounter() const ++ { ++ return m_commandBufferManager->GetCompletedFenceCounter(); ++ } ++ ++ uint64_t GetCurrentFenceCounter() const ++ { ++ return m_current_fence_counter.load(std::memory_order_acquire); ++ } ++ ++ void WaitForFenceCounter(u64 counter); ++ void SynchronizeSubmissionThread(); ++ ++ bool CheckLastPresentFail() { return m_commandBufferManager->CheckLastPresentFail(); } ++ VkResult GetLastPresentResult() const { return m_commandBufferManager->GetLastPresentResult(); } ++ bool CheckLastPresentDone() { return m_commandBufferManager->CheckLastPresentDone(); } ++ ++ void SubmitCommandBuffer(bool submit_on_worker_thread, bool wait_for_completion, ++ VkSwapchainKHR present_swap_chain = VK_NULL_HANDLE, ++ uint32_t present_image_index = 0xFFFFFFFF); ++ ++private: ++ void WorkerThread(); ++ void AcquireNewChunk(); ++ ++ std::unique_ptr m_commandBufferManager; ++ ++ std::unique_ptr m_chunk; ++ ++ std::thread m_worker; ++ std::unique_ptr m_submit_loop; ++ ++ std::atomic m_current_fence_counter = 1; ++ ++ std::queue> m_work_queue; ++ std::mutex m_work_mutex; ++ std::condition_variable m_idle_condvar; ++ bool m_worker_idle{true}; ++ ++ std::vector> m_chunk_reserve; ++ std::mutex m_reserve_mutex; ++}; ++ ++extern std::unique_ptr g_scheduler; ++} // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKStreamBuffer.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKStreamBuffer.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKStreamBuffer.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKStreamBuffer.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -12,6 +12,7 @@ + #include "Common/MsgHandler.h" + + #include "VideoBackends/Vulkan/CommandBufferManager.h" ++#include "VideoBackends/Vulkan/VKScheduler.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + + namespace Vulkan +@@ -24,7 +25,13 @@ StreamBuffer::~StreamBuffer() + { + // VMA_ALLOCATION_CREATE_MAPPED_BIT automatically handles unmapping for us + if (m_buffer != VK_NULL_HANDLE) +- g_command_buffer_mgr->DeferBufferDestruction(m_buffer, m_alloc); ++ { ++ g_scheduler->Record( ++ [c_buffer = m_buffer, c_alloc = m_alloc](CommandBufferManager* command_buffer_mgr) { ++ if (c_buffer != VK_NULL_HANDLE) ++ command_buffer_mgr->DeferBufferDestruction(c_buffer, c_alloc); ++ }); ++ } + } + + std::unique_ptr StreamBuffer::Create(VkBufferUsageFlags usage, u32 size) +@@ -75,8 +82,11 @@ bool StreamBuffer::AllocateBuffer() + + // Destroy the backings for the buffer after the command buffer executes + // VMA_ALLOCATION_CREATE_MAPPED_BIT automatically handles unmapping for us +- if (m_buffer != VK_NULL_HANDLE) +- g_command_buffer_mgr->DeferBufferDestruction(m_buffer, m_alloc); ++ g_scheduler->Record( ++ [c_buffer = m_buffer, c_alloc = m_alloc](CommandBufferManager* command_buffer_mgr) { ++ if (c_buffer != VK_NULL_HANDLE) ++ command_buffer_mgr->DeferBufferDestruction(c_buffer, c_alloc); ++ }); + + // Replace with the new buffer + m_buffer = buffer; +@@ -175,7 +185,7 @@ void StreamBuffer::UpdateCurrentFencePos + return; + + // Has the offset changed since the last fence? +- const u64 counter = g_command_buffer_mgr->GetCurrentFenceCounter(); ++ const u64 counter = g_scheduler->GetCurrentFenceCounter(); + if (!m_tracked_fences.empty() && m_tracked_fences.back().first == counter) + { + // Still haven't executed a command buffer, so just update the offset. +@@ -193,7 +203,7 @@ void StreamBuffer::UpdateGPUPosition() + auto start = m_tracked_fences.begin(); + auto end = start; + +- const u64 completed_counter = g_command_buffer_mgr->GetCompletedFenceCounter(); ++ const u64 completed_counter = g_scheduler->GetCompletedFenceCounter(); + while (end != m_tracked_fences.end() && completed_counter >= end->first) + { + m_current_gpu_position = end->second; +@@ -266,14 +276,13 @@ bool StreamBuffer::WaitForClearSpace(u32 + + // Did any fences satisfy this condition? + // Has the command buffer been executed yet? If not, the caller should execute it. +- if (iter == m_tracked_fences.end() || +- iter->first == g_command_buffer_mgr->GetCurrentFenceCounter()) ++ if (iter == m_tracked_fences.end() || iter->first == g_scheduler->GetCurrentFenceCounter()) + { + return false; + } + + // Wait until this fence is signaled. This will fire the callback, updating the GPU position. +- g_command_buffer_mgr->WaitForFenceCounter(iter->first); ++ g_scheduler->WaitForFenceCounter(iter->first); + m_tracked_fences.erase(m_tracked_fences.begin(), + m_current_offset == iter->second ? m_tracked_fences.end() : ++iter); + m_current_offset = new_offset; +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -13,9 +13,9 @@ + + #include "VideoBackends/Vulkan/CommandBufferManager.h" + #include "VideoBackends/Vulkan/ObjectCache.h" ++#include "VideoBackends/Vulkan/VKScheduler.h" + #include "VideoBackends/Vulkan/VKTexture.h" + #include "VideoBackends/Vulkan/VulkanContext.h" +-#include "VideoCommon/RenderBase.h" + + #if defined(VK_USE_PLATFORM_XLIB_KHR) + #include +@@ -25,15 +25,18 @@ namespace Vulkan + { + SwapChain::SwapChain(const WindowSystemInfo& wsi, VkSurfaceKHR surface, bool vsync) + : m_wsi(wsi), m_surface(surface), m_vsync_enabled(vsync), +- m_fullscreen_supported(g_vulkan_context->SupportsExclusiveFullscreen(wsi, surface)) ++ m_fullscreen_supported(g_vulkan_context->SupportsExclusiveFullscreen(wsi, surface)), ++ m_width(wsi.render_surface_width), m_height(wsi.render_surface_height) + { + } + + SwapChain::~SwapChain() + { ++ g_scheduler->SyncWorker(); + DestroySwapChainImages(); + DestroySwapChain(); + DestroySurface(); ++ DestroySemaphores(); + } + + VkSurfaceKHR SwapChain::CreateVulkanSurface(VkInstance instance, const WindowSystemInfo& wsi) +@@ -84,6 +87,29 @@ VkSurfaceKHR SwapChain::CreateVulkanSurf + } + #endif + ++#if defined(VK_USE_PLATFORM_WAYLAND_KHR) ++ if (wsi.type == WindowSystemType::Wayland) ++ { ++ VkWaylandSurfaceCreateInfoKHR surface_create_info = { ++ VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, // VkStructureType sType ++ nullptr, // const void* pNext ++ 0, // VkWaylandSurfaceCreateFlagsKHR flags ++ static_cast(wsi.display_connection), // struct wl_display* display ++ static_cast(wsi.render_surface) // struct wl_surface* surface ++ }; ++ ++ VkSurfaceKHR surface; ++ VkResult res = vkCreateWaylandSurfaceKHR(instance, &surface_create_info, nullptr, &surface); ++ if (res != VK_SUCCESS) ++ { ++ LOG_VULKAN_ERROR(res, "vkCreateWaylandSurfaceKHR failed: "); ++ return VK_NULL_HANDLE; ++ } ++ ++ return surface; ++ } ++#endif ++ + #if defined(VK_USE_PLATFORM_ANDROID_KHR) + if (wsi.type == WindowSystemType::Android) + { +@@ -132,7 +158,8 @@ std::unique_ptr SwapChain::Cr + bool vsync) + { + std::unique_ptr swap_chain = std::make_unique(wsi, surface, vsync); +- if (!swap_chain->CreateSwapChain() || !swap_chain->SetupSwapChainImages()) ++ if (!swap_chain->CreateSwapChain() || !swap_chain->SetupSwapChainImages() || ++ !swap_chain->CreateSemaphores()) + return nullptr; + + return swap_chain; +@@ -237,6 +264,23 @@ bool SwapChain::SelectPresentMode() + return true; + } + ++bool SwapChain::CreateSemaphores() ++{ ++ static constexpr VkSemaphoreCreateInfo semaphore_create_info = { ++ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, nullptr, 0}; ++ for (VkSemaphore& semaphore : m_semaphores) ++ { ++ VkResult res = vkCreateSemaphore(g_vulkan_context->GetDevice(), &semaphore_create_info, nullptr, ++ &semaphore); ++ if (res != VK_SUCCESS) ++ { ++ LOG_VULKAN_ERROR(res, "vkCreateSemaphore failed: "); ++ return false; ++ } ++ } ++ return true; ++} ++ + bool SwapChain::CreateSwapChain() + { + // Look up surface properties to determine image count and dimensions +@@ -265,8 +309,8 @@ bool SwapChain::CreateSwapChain() + VkExtent2D size = surface_capabilities.currentExtent; + if (size.width == UINT32_MAX) + { +- size.width = std::max(g_renderer->GetBackbufferWidth(), 1); +- size.height = std::max(g_renderer->GetBackbufferHeight(), 1); ++ size.width = static_cast(m_wsi.render_surface_width); ++ size.height = static_cast(m_wsi.render_surface_height); + } + size.width = std::clamp(size.width, surface_capabilities.minImageExtent.width, + surface_capabilities.maxImageExtent.width); +@@ -452,20 +496,33 @@ void SwapChain::DestroySwapChain() + m_swap_chain = VK_NULL_HANDLE; + } + +-VkResult SwapChain::AcquireNextImage() ++void SwapChain::DestroySemaphores() + { +- VkResult res = vkAcquireNextImageKHR(g_vulkan_context->GetDevice(), m_swap_chain, UINT64_MAX, +- g_command_buffer_mgr->GetCurrentCommandBufferSemaphore(), +- VK_NULL_HANDLE, &m_current_swap_chain_image_index); ++ for (VkSemaphore semaphore : m_semaphores) ++ { ++ if (semaphore != VK_NULL_HANDLE) ++ { ++ vkDestroySemaphore(g_vulkan_context->GetDevice(), semaphore, nullptr); ++ } ++ } ++} ++ ++VkResult SwapChain::AcquireNextImage(VkSemaphore semaphore) ++{ ++ VkResult res = ++ vkAcquireNextImageKHR(g_vulkan_context->GetDevice(), m_swap_chain, UINT64_MAX, semaphore, ++ VK_NULL_HANDLE, &m_current_swap_chain_image_index); + if (res != VK_SUCCESS && res != VK_ERROR_OUT_OF_DATE_KHR && res != VK_SUBOPTIMAL_KHR) + LOG_VULKAN_ERROR(res, "vkAcquireNextImageKHR failed: "); + + return res; + } + +-bool SwapChain::ResizeSwapChain() ++bool SwapChain::ResizeSwapChain(int window_width, int window_height) + { + DestroySwapChainImages(); ++ m_wsi.render_surface_width = window_width; ++ m_wsi.render_surface_height = window_height; + if (!CreateSwapChain() || !SetupSwapChainImages()) + { + PanicAlertFmt("Failed to re-configure swap chain images, this is fatal (for now)"); +@@ -531,7 +588,7 @@ bool SwapChain::SetFullscreenState(bool + #endif + } + +-bool SwapChain::RecreateSurface(void* native_handle) ++bool SwapChain::RecreateSurface(void* native_handle, int window_width, int window_height) + { + // Destroy the old swap chain, images, and surface. + DestroySwapChainImages(); +@@ -540,6 +597,8 @@ bool SwapChain::RecreateSurface(void* na + + // Re-create the surface with the new native handle + m_wsi.render_surface = native_handle; ++ m_wsi.render_surface_width = window_width; ++ m_wsi.render_surface_height = window_height; + m_surface = CreateVulkanSurface(g_vulkan_context->GetVulkanInstance(), m_wsi); + if (m_surface == VK_NULL_HANDLE) + return false; +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKSwapChain.h dolphin/Source/Core/VideoBackends/Vulkan/VKSwapChain.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKSwapChain.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKSwapChain.h 2024-03-05 04:06:08.240295843 +0000 +@@ -3,6 +3,7 @@ + + #pragma once + ++#include + #include + #include + +@@ -50,10 +51,10 @@ public: + { + return m_swap_chain_images[m_current_swap_chain_image_index].framebuffer.get(); + } +- VkResult AcquireNextImage(); ++ VkResult AcquireNextImage(VkSemaphore semaphore); + +- bool RecreateSurface(void* native_handle); +- bool ResizeSwapChain(); ++ bool RecreateSurface(void* native_handle, int window_width, int window_height); ++ bool ResizeSwapChain(int window_width, int window_height); + bool RecreateSwapChain(); + + // Change vsync enabled state. This may fail as it causes a swapchain recreation. +@@ -70,10 +71,19 @@ public: + // Updates the fullscreen state. Must call on-thread. + bool SetFullscreenState(bool state); + ++ VkSemaphore GetNextSemaphore() ++ { ++ m_semaphore_index = (m_semaphore_index + 1) % NUM_COMMAND_BUFFERS; ++ return m_semaphores[m_semaphore_index]; ++ } ++ + private: + bool SelectSurfaceFormat(); + bool SelectPresentMode(); + ++ bool CreateSemaphores(); ++ void DestroySemaphores(); ++ + bool CreateSwapChain(); + void DestroySwapChain(); + +@@ -103,6 +113,9 @@ private: + std::vector m_swap_chain_images; + u32 m_current_swap_chain_image_index = 0; + ++ std::array m_semaphores = {}; ++ u32 m_semaphore_index = 0; ++ + u32 m_width = 0; + u32 m_height = 0; + u32 m_layers = 0; +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKTexture.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKTexture.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKTexture.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKTexture.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -21,6 +21,7 @@ + #include "VideoBackends/Vulkan/VKStreamBuffer.h" + #include "VideoBackends/Vulkan/VulkanContext.h" + ++#include "VKScheduler.h" + #include "VideoCommon/DriverDetails.h" + #include "VideoCommon/VideoConfig.h" + +@@ -45,14 +46,17 @@ VKTexture::VKTexture(const TextureConfig + + VKTexture::~VKTexture() + { +- StateTracker::GetInstance()->UnbindTexture(m_view); +- g_command_buffer_mgr->DeferImageViewDestruction(m_view); ++ g_scheduler->Record([c_view = m_view, c_image = m_image, ++ c_alloc = m_alloc](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->UnbindTexture(c_view); ++ command_buffer_mgr->DeferImageViewDestruction(c_view); + +- // If we don't have device memory allocated, the image is not owned by us (e.g. swapchain) +- if (m_alloc != VK_NULL_HANDLE) +- { +- g_command_buffer_mgr->DeferImageDestruction(m_image, m_alloc); +- } ++ // If we don't have device memory allocated, the image is not owned by us (e.g. swapchain) ++ if (c_alloc != VK_NULL_HANDLE) ++ { ++ command_buffer_mgr->DeferImageDestruction(c_image, c_alloc); ++ } ++ }); + } + + std::unique_ptr VKTexture::Create(const TextureConfig& tex_config, std::string_view name) +@@ -265,30 +269,35 @@ void VKTexture::CopyRectangleFromTexture + static_cast(dst_rect.GetHeight()) <= m_config.height, + "Dest rect is too large for CopyRectangleFromTexture"); + +- const u32 copy_layer_count = 1; +- +- VkImageCopy image_copy = { +- {VK_IMAGE_ASPECT_COLOR_BIT, src_level, src_layer, copy_layer_count}, +- {src_rect.left, src_rect.top, 0}, +- {VK_IMAGE_ASPECT_COLOR_BIT, dst_level, dst_layer, copy_layer_count}, +- {dst_rect.left, dst_rect.top, 0}, +- {static_cast(src_rect.GetWidth()), static_cast(src_rect.GetHeight()), 1}}; +- +- // Must be called outside of a render pass. +- StateTracker::GetInstance()->EndRenderPass(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ // Must be called outside of a render pass. ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); + + const VkImageLayout old_src_layout = src_texture->GetLayout(); +- src_texture->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); +- TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +- +- vkCmdCopyImage(g_command_buffer_mgr->GetCurrentCommandBuffer(), src_texture->m_image, +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_image, +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ src_texture->TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); ++ TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); ++ ++ g_scheduler->Record([c_src_image = src_texture->GetImage(), c_image = m_image, ++ c_src_level = src_level, c_src_layer = src_layer, ++ c_src_layers = src_texture->GetLayers(), c_src_rect = src_rect, ++ c_dst_level = dst_level, c_dst_layer = dst_layer, ++ c_dst_layers = m_config.layers, ++ c_dst_rect = dst_rect](CommandBufferManager* command_buffer_mgr) { ++ VkImageCopy image_copy = {{VK_IMAGE_ASPECT_COLOR_BIT, c_src_level, c_src_layer, c_src_layers}, ++ {c_src_rect.left, c_src_rect.top, 0}, ++ {VK_IMAGE_ASPECT_COLOR_BIT, c_dst_level, c_dst_layer, c_dst_layers}, ++ {c_dst_rect.left, c_dst_rect.top, 0}, ++ {static_cast(c_src_rect.GetWidth()), ++ static_cast(c_src_rect.GetHeight()), 1}}; ++ ++ vkCmdCopyImage(command_buffer_mgr->GetCurrentCommandBuffer(), c_src_image, ++ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, c_image, ++ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ }); + + // Only restore the source layout. Destination is restored by FinishedRendering(). +- src_texture->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), old_src_layout); ++ src_texture->TransitionToLayout(old_src_layout); + } + + void VKTexture::ResolveFromTexture(const AbstractTexture* src, const MathUtil::Rectangle& rect, +@@ -301,24 +310,28 @@ void VKTexture::ResolveFromTexture(const + rect.top + rect.GetHeight() <= static_cast(srcentry->m_config.height)); + + // Resolving is considered to be a transfer operation. +- StateTracker::GetInstance()->EndRenderPass(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); + VkImageLayout old_src_layout = srcentry->m_layout; +- srcentry->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); +- TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +- +- VkImageResolve resolve = { +- {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, // srcSubresource +- {rect.left, rect.top, 0}, // srcOffset +- {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, // dstSubresource +- {rect.left, rect.top, 0}, // dstOffset +- {static_cast(rect.GetWidth()), static_cast(rect.GetHeight()), 1} // extent +- }; +- vkCmdResolveImage(g_command_buffer_mgr->GetCurrentCommandBuffer(), srcentry->m_image, +- srcentry->m_layout, m_image, m_layout, 1, &resolve); ++ srcentry->TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); ++ TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + +- srcentry->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), old_src_layout); ++ g_scheduler->Record([c_src_image = srcentry->GetImage(), c_image = m_image, ++ c_src_layout = srcentry->GetLayout(), c_dst_layout = m_layout, c_rect = rect, ++ c_layer = layer, c_level = level](CommandBufferManager* command_buffer_mgr) { ++ VkImageResolve resolve = { ++ {VK_IMAGE_ASPECT_COLOR_BIT, c_level, c_layer, 1}, // srcSubresource ++ {c_rect.left, c_rect.top, 0}, // srcOffset ++ {VK_IMAGE_ASPECT_COLOR_BIT, c_level, c_layer, 1}, // dstSubresource ++ {c_rect.left, c_rect.top, 0}, // dstOffset ++ {static_cast(c_rect.GetWidth()), static_cast(c_rect.GetHeight()), 1} // extent ++ }; ++ vkCmdResolveImage(command_buffer_mgr->GetCurrentCommandBuffer(), c_src_image, c_src_layout, ++ c_image, c_dst_layout, 1, &resolve); ++ }); ++ ++ srcentry->TransitionToLayout(old_src_layout); + } + + void VKTexture::Load(u32 level, u32 width, u32 height, u32 row_length, const u8* buffer, +@@ -344,8 +357,7 @@ void VKTexture::Load(u32 level, u32 widt + // When the last mip level is uploaded, we transition to SHADER_READ_ONLY, ready for use. This is + // because we can't transition in a render pass, and we don't necessarily know when this texture + // is going to be used. +- TransitionToLayout(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); ++ TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true); + + // For unaligned textures, we can save some memory in the transfer buffer by skipping the rows + // that lie outside of the texture's dimensions. +@@ -396,26 +408,30 @@ void VKTexture::Load(u32 level, u32 widt + temp_buffer->Unmap(); + } + +- // Copy from the streaming buffer to the actual image. +- VkBufferImageCopy image_copy = { +- upload_buffer_offset, // VkDeviceSize bufferOffset +- row_length, // uint32_t bufferRowLength +- 0, // uint32_t bufferImageHeight +- {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, // VkImageSubresourceLayers imageSubresource +- {0, 0, 0}, // VkOffset3D imageOffset +- {width, height, 1} // VkExtent3D imageExtent +- }; +- vkCmdCopyBufferToImage(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), upload_buffer, +- m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ g_scheduler->Record([c_upload_buffer_offset = upload_buffer_offset, c_row_length = row_length, ++ c_level = level, c_width = width, c_height = height, ++ c_upload_buffer = upload_buffer, ++ c_image = m_image](CommandBufferManager* command_buffer_mgr) { ++ // Copy from the streaming buffer to the actual image. ++ VkBufferImageCopy image_copy = { ++ c_upload_buffer_offset, // VkDeviceSize bufferOffset ++ c_row_length, // uint32_t bufferRowLength ++ 0, // uint32_t bufferImageHeight ++ {VK_IMAGE_ASPECT_COLOR_BIT, c_level, 0, 1}, // VkImageSubresourceLayers imageSubresource ++ {0, 0, 0}, // VkOffset3D imageOffset ++ {c_width, c_height, 1} // VkExtent3D imageExtent ++ }; ++ vkCmdCopyBufferToImage(command_buffer_mgr->GetCurrentInitCommandBuffer(), c_upload_buffer, ++ c_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ }); + + // Preemptively transition to shader read only after uploading the last mip level, as we're + // likely finished with writes to this texture for now. We can't do this in common with a + // FinishedRendering() call because the upload happens in the init command buffer, and we + // don't want to interrupt the render pass with calls which were executed ages before. +- if (level == (m_config.levels - 1) && layer == (m_config.layers - 1)) ++ if (level == (m_config.levels - 1)) + { +- TransitionToLayout(g_command_buffer_mgr->GetCurrentInitCommandBuffer(), +- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ++ TransitionToLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, true); + } + } + +@@ -424,9 +440,10 @@ void VKTexture::FinishedRendering() + if (m_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + return; + +- StateTracker::GetInstance()->EndRenderPass(); +- TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); ++ TransitionToLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + + void VKTexture::OverrideImageLayout(VkImageLayout new_layout) +@@ -434,9 +451,9 @@ void VKTexture::OverrideImageLayout(VkIm + m_layout = new_layout; + } + +-void VKTexture::TransitionToLayout(VkCommandBuffer command_buffer, VkImageLayout new_layout) const ++void VKTexture::TransitionToLayout(VkImageLayout new_layout, bool init_command_buffer) const + { +- if (m_layout == new_layout) ++ if (m_layout == new_layout) [[likely]] + return; + + VkImageMemoryBarrier barrier = { +@@ -573,14 +590,19 @@ void VKTexture::TransitionToLayout(VkCom + } + m_compute_layout = ComputeImageLayout::Undefined; + +- vkCmdPipelineBarrier(command_buffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, +- &barrier); ++ g_scheduler->Record([c_src_stage_mask = srcStageMask, c_dst_stage_mask = dstStageMask, ++ c_barrier = barrier, ++ init_command_buffer](CommandBufferManager* command_buffer_mgr) { ++ vkCmdPipelineBarrier(init_command_buffer ? command_buffer_mgr->GetCurrentInitCommandBuffer() : ++ command_buffer_mgr->GetCurrentCommandBuffer(), ++ c_src_stage_mask, c_dst_stage_mask, 0, 0, nullptr, 0, nullptr, 1, ++ &c_barrier); ++ }); + + m_layout = new_layout; + } + +-void VKTexture::TransitionToLayout(VkCommandBuffer command_buffer, +- ComputeImageLayout new_layout) const ++void VKTexture::TransitionToLayout(ComputeImageLayout new_layout, bool init_command_buffer) const + { + ASSERT(new_layout != ComputeImageLayout::Undefined); + if (m_compute_layout == new_layout) +@@ -678,8 +700,14 @@ void VKTexture::TransitionToLayout(VkCom + m_layout = barrier.newLayout; + m_compute_layout = new_layout; + +- vkCmdPipelineBarrier(command_buffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, +- &barrier); ++ g_scheduler->Record([c_src_stage_mask = srcStageMask, c_dst_stage_mask = dstStageMask, ++ c_barrier = barrier, ++ init_command_buffer](CommandBufferManager* command_buffer_mgr) { ++ vkCmdPipelineBarrier(init_command_buffer ? command_buffer_mgr->GetCurrentInitCommandBuffer() : ++ command_buffer_mgr->GetCurrentCommandBuffer(), ++ c_src_stage_mask, c_dst_stage_mask, 0, 0, nullptr, 0, nullptr, 1, ++ &c_barrier); ++ }); + } + + VKStagingTexture::VKStagingTexture(PrivateTag, StagingTextureType type, const TextureConfig& config, +@@ -694,7 +722,11 @@ VKStagingTexture::~VKStagingTexture() + { + if (m_linear_image != VK_NULL_HANDLE) + { +- g_command_buffer_mgr->DeferImageDestruction(m_linear_image, m_linear_image_alloc); ++ g_scheduler->Record( ++ [c_linear_image = m_linear_image, ++ c_linear_image_alloc = m_linear_image_alloc](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->DeferImageDestruction(c_linear_image, c_linear_image_alloc); ++ }); + } + } + +@@ -816,12 +848,12 @@ void VKStagingTexture::CopyFromTexture(c + src_rect.top >= 0 && static_cast(src_rect.bottom) <= src_tex->GetHeight()); + ASSERT(dst_rect.left >= 0 && static_cast(dst_rect.right) <= m_config.width && + dst_rect.top >= 0 && static_cast(dst_rect.bottom) <= m_config.height); +- +- StateTracker::GetInstance()->EndRenderPass(); ++ g_scheduler->Record([](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->EndRenderPass(); ++ }); + + VkImageLayout old_layout = src_tex->GetLayout(); +- src_tex->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); ++ src_tex->TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + // Issue the image->buffer copy, but delay it for now. + VkBufferImageCopy image_copy = {}; +@@ -844,15 +876,17 @@ void VKStagingTexture::CopyFromTexture(c + image_copy.imageOffset = {0, 0, 0}; + } + +- vkCmdCopyImageToBuffer(g_command_buffer_mgr->GetCurrentCommandBuffer(), src_image, +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_staging_buffer->GetBuffer(), 1, +- &image_copy); ++ g_scheduler->Record([c_src_image = src_image, c_dst_buffer = m_staging_buffer->GetBuffer(), ++ c_image_copy = image_copy](CommandBufferManager* command_buffer_mgr) { ++ vkCmdCopyImageToBuffer(command_buffer_mgr->GetCurrentCommandBuffer(), c_src_image, ++ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, c_dst_buffer, 1, &c_image_copy); ++ }); + + // Restore old source texture layout. +- src_tex->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), old_layout); ++ src_tex->TransitionToLayout(old_layout); + + m_needs_flush = true; +- m_flush_fence_counter = g_command_buffer_mgr->GetCurrentFenceCounter(); ++ m_flush_fence_counter = g_scheduler->GetCurrentFenceCounter(); + } + + void VKStagingTexture::CopyFromTextureToLinearImage(const VKTexture* src_tex, +@@ -864,44 +898,49 @@ void VKStagingTexture::CopyFromTextureTo + // with optimal tiling (VK_IMAGE_TILING_OPTIMAL) to a buffer. + // That allocation is very slow, so we just do it ourself and reuse the intermediate image. + +- const VkImageAspectFlags aspect = VKTexture::GetImageViewAspectForFormat(src_tex->GetFormat()); +- +- VkImageMemoryBarrier linear_image_barrier = {}; +- linear_image_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +- linear_image_barrier.pNext = nullptr; +- linear_image_barrier.srcAccessMask = 0; +- linear_image_barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; +- linear_image_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; +- linear_image_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +- linear_image_barrier.image = m_linear_image; +- linear_image_barrier.subresourceRange = {aspect, 0, 1, 0, 1}; +- vkCmdPipelineBarrier(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, +- nullptr, 0, nullptr, 1, &linear_image_barrier); +- +- VkImageBlit blit; +- blit.srcSubresource = {aspect, src_level, src_layer, 1}; +- blit.dstSubresource.layerCount = 1; +- blit.dstSubresource.baseArrayLayer = 0; +- blit.dstSubresource.mipLevel = 0; +- blit.dstSubresource.aspectMask = linear_image_barrier.subresourceRange.aspectMask; +- blit.srcOffsets[0] = {src_rect.left, src_rect.top, 0}; +- blit.srcOffsets[1] = {static_cast(blit.srcOffsets[0].x + src_rect.GetWidth()), +- static_cast(blit.srcOffsets[0].y + src_rect.GetHeight()), 1}; +- blit.dstOffsets[0] = {0, 0, 0}; +- blit.dstOffsets[1] = {dst_rect.GetWidth(), dst_rect.GetHeight(), 1u}; +- +- vkCmdBlitImage(g_command_buffer_mgr->GetCurrentCommandBuffer(), src_tex->GetImage(), +- VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_linear_image, +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); +- +- linear_image_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +- linear_image_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +- linear_image_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +- +- vkCmdPipelineBarrier(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, +- nullptr, 0, nullptr, 1, &linear_image_barrier); ++ g_scheduler->Record([c_linear_image = m_linear_image, c_src_image = src_tex->GetImage(), ++ c_src_format = src_tex->GetFormat(), c_src_rect = src_rect, ++ c_dst_rect = dst_rect, c_src_layer = src_layer, ++ c_src_level = src_level](CommandBufferManager* command_buffer_mgr) { ++ const VkImageAspectFlags aspect = VKTexture::GetImageViewAspectForFormat(c_src_format); ++ ++ VkImageMemoryBarrier linear_image_barrier = {}; ++ linear_image_barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; ++ linear_image_barrier.pNext = nullptr; ++ linear_image_barrier.srcAccessMask = 0; ++ linear_image_barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; ++ linear_image_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; ++ linear_image_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; ++ linear_image_barrier.image = c_linear_image; ++ linear_image_barrier.subresourceRange = {aspect, 0, 1, 0, 1}; ++ vkCmdPipelineBarrier(command_buffer_mgr->GetCurrentCommandBuffer(), ++ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, ++ nullptr, 0, nullptr, 1, &linear_image_barrier); ++ ++ VkImageBlit blit; ++ blit.srcSubresource = {aspect, c_src_level, c_src_layer, 1}; ++ blit.dstSubresource.layerCount = 1; ++ blit.dstSubresource.baseArrayLayer = 0; ++ blit.dstSubresource.mipLevel = 0; ++ blit.dstSubresource.aspectMask = linear_image_barrier.subresourceRange.aspectMask; ++ blit.srcOffsets[0] = {c_src_rect.left, c_src_rect.top, 0}; ++ blit.srcOffsets[1] = {static_cast(blit.srcOffsets[0].x + c_src_rect.GetWidth()), ++ static_cast(blit.srcOffsets[0].y + c_src_rect.GetHeight()), 1}; ++ blit.dstOffsets[0] = {0, 0, 0}; ++ blit.dstOffsets[1] = {c_dst_rect.GetWidth(), c_dst_rect.GetHeight(), 1u}; ++ ++ vkCmdBlitImage(command_buffer_mgr->GetCurrentCommandBuffer(), c_src_image, ++ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, c_linear_image, ++ VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); ++ ++ linear_image_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; ++ linear_image_barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; ++ linear_image_barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; ++ ++ vkCmdPipelineBarrier(command_buffer_mgr->GetCurrentCommandBuffer(), ++ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, ++ nullptr, 0, nullptr, 1, &linear_image_barrier); ++ }); + } + + void VKStagingTexture::CopyToTexture(const MathUtil::Rectangle& src_rect, AbstractTexture* dst, +@@ -919,32 +958,40 @@ void VKStagingTexture::CopyToTexture(con + + // Flush caches before copying. + m_staging_buffer->FlushCPUCache(); +- StateTracker::GetInstance()->EndRenderPass(); ++ ++ g_scheduler->Record([](CommandBufferManager* command_buffer_manager) { ++ command_buffer_manager->GetStateTracker()->EndRenderPass(); ++ }); + + VkImageLayout old_layout = dst_tex->GetLayout(); +- dst_tex->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); ++ dst_tex->TransitionToLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + +- // Issue the image->buffer copy, but delay it for now. +- VkBufferImageCopy image_copy = {}; +- image_copy.bufferOffset = +- static_cast(static_cast(src_rect.top) * m_config.GetStride() + +- static_cast(src_rect.left) * m_texel_size); +- image_copy.bufferRowLength = static_cast(m_config.width); +- image_copy.bufferImageHeight = 0; +- image_copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, dst_level, dst_layer, 1}; +- image_copy.imageOffset = {dst_rect.left, dst_rect.top, 0}; +- image_copy.imageExtent = {static_cast(dst_rect.GetWidth()), +- static_cast(dst_rect.GetHeight()), 1u}; +- vkCmdCopyBufferToImage(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- m_staging_buffer->GetBuffer(), dst_tex->GetImage(), +- VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ g_scheduler->Record( ++ [c_src_rect = src_rect, c_dst_rect = dst_rect, c_dst_layer = dst_layer, ++ c_dst_level = dst_level, c_width = m_config.width, c_height = m_config.height, ++ c_stride = m_config.GetStride(), c_texel_size = m_texel_size, ++ c_staging_buffer = m_staging_buffer->GetBuffer(), ++ c_dst_image = dst_tex->GetImage()](CommandBufferManager* command_buffer_mgr) { ++ // Issue the image->buffer copy, but delay it for now. ++ VkBufferImageCopy image_copy = {}; ++ image_copy.bufferOffset = ++ static_cast(static_cast(c_src_rect.top) * c_stride + ++ static_cast(c_src_rect.left) * c_texel_size); ++ image_copy.bufferRowLength = static_cast(c_width); ++ image_copy.bufferImageHeight = 0; ++ image_copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, c_dst_level, c_dst_layer, 1}; ++ image_copy.imageOffset = {c_dst_rect.left, c_dst_rect.top, 0}; ++ image_copy.imageExtent = {static_cast(c_dst_rect.GetWidth()), ++ static_cast(c_dst_rect.GetHeight()), 1u}; ++ vkCmdCopyBufferToImage(command_buffer_mgr->GetCurrentCommandBuffer(), c_staging_buffer, ++ c_dst_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &image_copy); ++ }); + + // Restore old source texture layout. +- dst_tex->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), old_layout); ++ dst_tex->TransitionToLayout(old_layout); + + m_needs_flush = true; +- m_flush_fence_counter = g_command_buffer_mgr->GetCurrentFenceCounter(); ++ m_flush_fence_counter = g_scheduler->GetCurrentFenceCounter(); + } + + bool VKStagingTexture::Map() +@@ -964,7 +1011,7 @@ void VKStagingTexture::Flush() + return; + + // Is this copy in the current command buffer? +- if (g_command_buffer_mgr->GetCurrentFenceCounter() == m_flush_fence_counter) ++ if (g_scheduler->GetCurrentFenceCounter() == m_flush_fence_counter) + { + // Execute the command buffer and wait for it to finish. + Renderer::GetInstance()->ExecuteCommandBuffer(false, true); +@@ -972,7 +1019,7 @@ void VKStagingTexture::Flush() + else + { + // Wait for the GPU to finish with it. +- g_command_buffer_mgr->WaitForFenceCounter(m_flush_fence_counter); ++ g_scheduler->WaitForFenceCounter(m_flush_fence_counter); + } + + // For readback textures, invalidate the CPU cache as there is new data there. +@@ -998,7 +1045,11 @@ VKFramebuffer::VKFramebuffer(VKTexture* + + VKFramebuffer::~VKFramebuffer() + { +- g_command_buffer_mgr->DeferFramebufferDestruction(m_fb); ++ g_scheduler->SyncWorker(); // TODO ++ ++ g_scheduler->Record([c_fb = m_fb](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->DeferFramebufferDestruction(c_fb); ++ }); + } + + std::unique_ptr VKFramebuffer::Create(VKTexture* color_attachment, +@@ -1067,15 +1118,13 @@ void VKFramebuffer::TransitionForRender( + if (m_color_attachment) + { + static_cast(m_color_attachment) +- ->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ++ ->TransitionToLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + } + + if (m_depth_attachment) + { + static_cast(m_depth_attachment) +- ->TransitionToLayout(g_command_buffer_mgr->GetCurrentCommandBuffer(), +- VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); ++ ->TransitionToLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + } + } + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKTexture.h dolphin/Source/Core/VideoBackends/Vulkan/VKTexture.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKTexture.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKTexture.h 2024-03-05 04:06:08.240295843 +0000 +@@ -67,8 +67,8 @@ public: + // irrelevant and will not be loaded. + void OverrideImageLayout(VkImageLayout new_layout); + +- void TransitionToLayout(VkCommandBuffer command_buffer, VkImageLayout new_layout) const; +- void TransitionToLayout(VkCommandBuffer command_buffer, ComputeImageLayout new_layout) const; ++ void TransitionToLayout(VkImageLayout new_layout, bool init_command_buffer = false) const; ++ void TransitionToLayout(ComputeImageLayout new_layout, bool init_command_buffer = false) const; + + private: + bool CreateView(VkImageViewType type); +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VKVertexManager.cpp dolphin/Source/Core/VideoBackends/Vulkan/VKVertexManager.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VKVertexManager.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VKVertexManager.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -15,6 +15,7 @@ + #include "VideoBackends/Vulkan/CommandBufferManager.h" + #include "VideoBackends/Vulkan/StateTracker.h" + #include "VideoBackends/Vulkan/VKRenderer.h" ++#include "VideoBackends/Vulkan/VKScheduler.h" + #include "VideoBackends/Vulkan/VKStreamBuffer.h" + #include "VideoBackends/Vulkan/VKVertexFormat.h" + #include "VideoBackends/Vulkan/VulkanContext.h" +@@ -67,11 +68,11 @@ bool VertexManager::Initialize() + + m_vertex_stream_buffer = + StreamBuffer::Create(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, +- VERTEX_STREAM_BUFFER_SIZE); ++ VERTEX_STREAM_BUFFER_SIZE * 2); + m_index_stream_buffer = +- StreamBuffer::Create(VK_BUFFER_USAGE_INDEX_BUFFER_BIT, INDEX_STREAM_BUFFER_SIZE); ++ StreamBuffer::Create(VK_BUFFER_USAGE_INDEX_BUFFER_BIT, INDEX_STREAM_BUFFER_SIZE * 2); + m_uniform_stream_buffer = +- StreamBuffer::Create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, UNIFORM_STREAM_BUFFER_SIZE); ++ StreamBuffer::Create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, UNIFORM_STREAM_BUFFER_SIZE * 2); + if (!m_vertex_stream_buffer || !m_index_stream_buffer || !m_uniform_stream_buffer) + { + PanicAlertFmt("Failed to allocate streaming buffers"); +@@ -121,13 +122,18 @@ bool VertexManager::Initialize() + + // Bind the buffers to all the known spots even if it's not used, to keep the driver happy. + UploadAllConstants(); +- StateTracker::GetInstance()->SetUtilityUniformBuffer(m_uniform_stream_buffer->GetBuffer(), 0, +- sizeof(VertexShaderConstants)); +- for (u32 i = 0; i < NUM_COMPUTE_TEXEL_BUFFERS; i++) +- { +- StateTracker::GetInstance()->SetTexelBuffer(i, +- m_texel_buffer_views[TEXEL_BUFFER_FORMAT_R8_UINT]); +- } ++ ++ g_scheduler->Record([c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_texel_buffer_view = m_texel_buffer_views[TEXEL_BUFFER_FORMAT_R8_UINT]]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetUtilityUniformBuffer(c_buffer, 0, ++ sizeof(VertexShaderConstants)); ++ ++ for (u32 i = 0; i < NUM_COMPUTE_TEXEL_BUFFERS; i++) ++ { ++ command_buffer_mgr->GetStateTracker()->SetTexelBuffer(i, c_texel_buffer_view); ++ } ++ }); + + return true; + } +@@ -189,10 +195,13 @@ void VertexManager::CommitBuffer(u32 num + ADDSTAT(g_stats.this_frame.bytes_vertex_streamed, static_cast(vertex_data_size)); + ADDSTAT(g_stats.this_frame.bytes_index_streamed, static_cast(index_data_size)); + +- StateTracker::GetInstance()->SetVertexBuffer(m_vertex_stream_buffer->GetBuffer(), 0, +- VERTEX_STREAM_BUFFER_SIZE); +- StateTracker::GetInstance()->SetIndexBuffer(m_index_stream_buffer->GetBuffer(), 0, +- VK_INDEX_TYPE_UINT16); ++ g_scheduler->Record([c_vertex_buffer = m_vertex_stream_buffer->GetBuffer(), ++ c_index_buffer = m_index_stream_buffer->GetBuffer()]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetVertexBuffer(c_vertex_buffer, 0, ++ VERTEX_STREAM_BUFFER_SIZE); ++ command_buffer_mgr->GetStateTracker()->SetIndexBuffer(c_index_buffer, 0, VK_INDEX_TYPE_UINT16); ++ }); + } + + void VertexManager::UploadUniforms() +@@ -210,9 +219,12 @@ void VertexManager::UpdateVertexShaderCo + if (!vertex_shader_manager.dirty || !ReserveConstantStorage()) + return; + +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_VS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset(), sizeof(VertexShaderConstants)); ++ g_scheduler->Record([c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_offset = m_uniform_stream_buffer->GetCurrentOffset()]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_VS, c_buffer, c_offset, sizeof(VertexShaderConstants)); ++ }); + std::memcpy(m_uniform_stream_buffer->GetCurrentHostPointer(), &vertex_shader_manager.constants, + sizeof(VertexShaderConstants)); + m_uniform_stream_buffer->CommitMemory(sizeof(VertexShaderConstants)); +@@ -228,9 +240,13 @@ void VertexManager::UpdateGeometryShader + if (!geometry_shader_manager.dirty || !ReserveConstantStorage()) + return; + +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_GS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset(), sizeof(GeometryShaderConstants)); ++ g_scheduler->Record([c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_offset = m_uniform_stream_buffer->GetCurrentOffset()]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_GS, c_buffer, c_offset, sizeof(GeometryShaderConstants)); ++ }); ++ + std::memcpy(m_uniform_stream_buffer->GetCurrentHostPointer(), &geometry_shader_manager.constants, + sizeof(GeometryShaderConstants)); + m_uniform_stream_buffer->CommitMemory(sizeof(GeometryShaderConstants)); +@@ -246,9 +262,13 @@ void VertexManager::UpdatePixelShaderCon + if (!pixel_shader_manager.dirty || !ReserveConstantStorage()) + return; + +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_PS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset(), sizeof(PixelShaderConstants)); ++ g_scheduler->Record([c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_offset = m_uniform_stream_buffer->GetCurrentOffset()]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_PS, c_buffer, c_offset, sizeof(PixelShaderConstants)); ++ }); ++ + std::memcpy(m_uniform_stream_buffer->GetCurrentHostPointer(), &pixel_shader_manager.constants, + sizeof(PixelShaderConstants)); + m_uniform_stream_buffer->CommitMemory(sizeof(PixelShaderConstants)); +@@ -299,18 +319,21 @@ void VertexManager::UploadAllConstants() + auto& geometry_shader_manager = system.GetGeometryShaderManager(); + + // Update bindings +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_PS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset() + pixel_constants_offset, +- sizeof(PixelShaderConstants)); +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_VS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset() + vertex_constants_offset, +- sizeof(VertexShaderConstants)); +- StateTracker::GetInstance()->SetGXUniformBuffer( +- UBO_DESCRIPTOR_SET_BINDING_GS, m_uniform_stream_buffer->GetBuffer(), +- m_uniform_stream_buffer->GetCurrentOffset() + geometry_constants_offset, +- sizeof(GeometryShaderConstants)); ++ g_scheduler->Record( ++ [c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_ps_offset = m_uniform_stream_buffer->GetCurrentOffset() + pixel_constants_offset, ++ c_vs_offset = m_uniform_stream_buffer->GetCurrentOffset() + vertex_constants_offset, ++ c_gs_offset = m_uniform_stream_buffer->GetCurrentOffset() + ++ geometry_constants_offset](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_PS, c_buffer, c_ps_offset, sizeof(PixelShaderConstants)); ++ ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_VS, c_buffer, c_vs_offset, sizeof(VertexShaderConstants)); ++ ++ command_buffer_mgr->GetStateTracker()->SetGXUniformBuffer( ++ UBO_DESCRIPTOR_SET_BINDING_VS, c_buffer, c_gs_offset, sizeof(GeometryShaderConstants)); ++ }); + + // Copy the actual data in + std::memcpy(m_uniform_stream_buffer->GetCurrentHostPointer() + pixel_constants_offset, +@@ -340,8 +363,11 @@ void VertexManager::UploadUtilityUniform + Renderer::GetInstance()->ExecuteCommandBuffer(false); + } + +- StateTracker::GetInstance()->SetUtilityUniformBuffer( +- m_uniform_stream_buffer->GetBuffer(), m_uniform_stream_buffer->GetCurrentOffset(), data_size); ++ g_scheduler->Record([c_buffer = m_uniform_stream_buffer->GetBuffer(), ++ c_offset = m_uniform_stream_buffer->GetCurrentOffset(), ++ c_size = data_size](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetUtilityUniformBuffer(c_buffer, c_offset, c_size); ++ }); + std::memcpy(m_uniform_stream_buffer->GetCurrentHostPointer(), data, data_size); + m_uniform_stream_buffer->CommitMemory(data_size); + ADDSTAT(g_stats.this_frame.bytes_uniform_streamed, data_size); +@@ -370,7 +396,10 @@ bool VertexManager::UploadTexelBuffer(co + *out_offset = static_cast(m_texel_stream_buffer->GetCurrentOffset()) / elem_size; + m_texel_stream_buffer->CommitMemory(data_size); + ADDSTAT(g_stats.this_frame.bytes_uniform_streamed, data_size); +- StateTracker::GetInstance()->SetTexelBuffer(0, m_texel_buffer_views[format]); ++ g_scheduler->Record([c_texel_buffer_view = ++ m_texel_buffer_views[format]](CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetTexelBuffer(0, c_texel_buffer_view); ++ }); + return true; + } + +@@ -407,8 +436,13 @@ bool VertexManager::UploadTexelBuffer(co + + m_texel_stream_buffer->CommitMemory(palette_byte_offset + palette_size); + ADDSTAT(g_stats.this_frame.bytes_uniform_streamed, palette_byte_offset + palette_size); +- StateTracker::GetInstance()->SetTexelBuffer(0, m_texel_buffer_views[format]); +- StateTracker::GetInstance()->SetTexelBuffer(1, m_texel_buffer_views[palette_format]); ++ ++ g_scheduler->Record([c_texel_buffer_view = m_texel_buffer_views[format], ++ c_palette_texel_buffer_view = m_texel_buffer_views[palette_format]]( ++ CommandBufferManager* command_buffer_mgr) { ++ command_buffer_mgr->GetStateTracker()->SetTexelBuffer(0, c_texel_buffer_view); ++ command_buffer_mgr->GetStateTracker()->SetTexelBuffer(1, c_palette_texel_buffer_view); ++ }); + return true; + } + } // namespace Vulkan +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp dolphin/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -274,6 +274,13 @@ bool VulkanContext::SelectInstanceExtens + return false; + } + #endif ++#if defined(VK_USE_PLATFORM_WAYLAND_KHR) ++ if (wstype == WindowSystemType::Wayland && ++ !AddExtension(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME, true)) ++ { ++ return false; ++ } ++#endif + #if defined(VK_USE_PLATFORM_ANDROID_KHR) + if (wstype == WindowSystemType::Android && + !AddExtension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, true)) +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanEntryPoints.inl dolphin/Source/Core/VideoBackends/Vulkan/VulkanEntryPoints.inl +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanEntryPoints.inl 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VulkanEntryPoints.inl 2024-03-05 04:06:08.240295843 +0000 +@@ -49,6 +49,11 @@ VULKAN_INSTANCE_ENTRY_POINT(vkCreateXlib + VULKAN_INSTANCE_ENTRY_POINT(vkGetPhysicalDeviceXlibPresentationSupportKHR, false) + #endif + ++#if defined(VK_USE_PLATFORM_WAYLAND_KHR) ++VULKAN_INSTANCE_ENTRY_POINT(vkCreateWaylandSurfaceKHR, false) ++VULKAN_INSTANCE_ENTRY_POINT(vkGetPhysicalDeviceWaylandPresentationSupportKHR, false) ++#endif ++ + #if defined(VK_USE_PLATFORM_ANDROID_KHR) + VULKAN_INSTANCE_ENTRY_POINT(vkCreateAndroidSurfaceKHR, false) + #endif +diff -rupN dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanLoader.h dolphin/Source/Core/VideoBackends/Vulkan/VulkanLoader.h +--- dolphin.orig/Source/Core/VideoBackends/Vulkan/VulkanLoader.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoBackends/Vulkan/VulkanLoader.h 2024-03-05 04:06:08.240295843 +0000 +@@ -13,6 +13,10 @@ + #define VK_USE_PLATFORM_XLIB_KHR + #endif + ++#if defined(HAVE_WAYLAND) ++#define VK_USE_PLATFORM_WAYLAND_KHR ++#endif ++ + #if defined(ANDROID) + #define VK_USE_PLATFORM_ANDROID_KHR + #endif +diff -rupN dolphin.orig/Source/Core/VideoCommon/FramebufferManager.cpp dolphin/Source/Core/VideoCommon/FramebufferManager.cpp +--- dolphin.orig/Source/Core/VideoCommon/FramebufferManager.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoCommon/FramebufferManager.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -630,7 +630,7 @@ void FramebufferManager::DestroyReadback + + bool FramebufferManager::CreateReadbackFramebuffer() + { +- if (g_renderer->GetEFBScale() != 1) ++ if (g_renderer->IsUnscaled()) + { + const TextureConfig color_config(IsUsingTiledEFBCache() ? m_efb_cache_tile_size : EFB_WIDTH, + IsUsingTiledEFBCache() ? m_efb_cache_tile_size : EFB_HEIGHT, 1, +@@ -651,7 +651,7 @@ bool FramebufferManager::CreateReadbackF + (IsUsingTiledEFBCache() && !g_ActiveConfig.backend_info.bSupportsPartialDepthCopies) || + !AbstractTexture::IsCompatibleDepthAndColorFormats(m_efb_depth_texture->GetFormat(), + GetEFBDepthCopyFormat()) || +- g_renderer->GetEFBScale() != 1) ++ g_renderer->IsUnscaled()) + { + const TextureConfig depth_config(IsUsingTiledEFBCache() ? m_efb_cache_tile_size : EFB_WIDTH, + IsUsingTiledEFBCache() ? m_efb_cache_tile_size : EFB_HEIGHT, 1, +@@ -731,7 +731,7 @@ void FramebufferManager::PopulateEFBCach + const MathUtil::Rectangle native_rect = g_renderer->ConvertEFBRectangle(rect); + AbstractTexture* src_texture = + depth ? ResolveEFBDepthTexture(native_rect) : ResolveEFBColorTexture(native_rect); +- if (g_renderer->GetEFBScale() != 1 || force_intermediate_copy) ++ if (g_renderer->IsUnscaled() || force_intermediate_copy) + { + // Downsample from internal resolution to 1x. + // TODO: This won't produce correct results at IRs above 2x. More samples are required. +@@ -918,7 +918,7 @@ void FramebufferManager::CreatePokeVerti + // GPU will expand the point to a quad. + const float cs_x = (static_cast(x) + 0.5f) * cs_pixel_width - 1.0f; + const float cs_y = 1.0f - (static_cast(y) + 0.5f) * cs_pixel_height; +- const float point_size = static_cast(g_renderer->GetEFBScale()); ++ const float point_size = g_renderer->GetEFBScalef(); + destination_list->push_back({{cs_x, cs_y, z, point_size}, color}); + return; + } +diff -rupN dolphin.orig/Source/Core/VideoCommon/RenderBase.cpp dolphin/Source/Core/VideoCommon/RenderBase.cpp +--- dolphin.orig/Source/Core/VideoCommon/RenderBase.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoCommon/RenderBase.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -364,19 +364,24 @@ void Renderer::RenderToXFB(u32 xfbAddr, + return; + } + +-unsigned int Renderer::GetEFBScale() const ++bool Renderer::IsUnscaled() const + { +- return m_efb_scale; ++ return m_efb_scale == 2; ++} ++ ++float Renderer::GetEFBScalef() const ++{ ++ return m_efb_scale / 2.0f; + } + + int Renderer::EFBToScaledX(int x) const + { +- return x * static_cast(m_efb_scale); ++ return x * static_cast(m_efb_scale) / 2; + } + + int Renderer::EFBToScaledY(int y) const + { +- return y * static_cast(m_efb_scale); ++ return y * static_cast(m_efb_scale) / 2; + } + + float Renderer::EFBToScaledXf(float x) const +@@ -391,7 +396,7 @@ float Renderer::EFBToScaledYf(float y) c + + std::tuple Renderer::CalculateTargetScale(int x, int y) const + { +- return std::make_tuple(x * static_cast(m_efb_scale), y * static_cast(m_efb_scale)); ++ return std::make_tuple(x * static_cast(m_efb_scale) / 2, y * static_cast(m_efb_scale) / 2); + } + + // return true if target size changed +@@ -711,16 +716,20 @@ bool Renderer::IsHeadless() const + return true; + } + +-void Renderer::ChangeSurface(void* new_surface_handle) ++void Renderer::ChangeSurface(void* new_surface_handle, int new_width, int new_height) + { + std::lock_guard lock(m_swap_mutex); + m_new_surface_handle = new_surface_handle; ++ m_new_surface_width = new_width; ++ m_new_surface_height = new_height; + m_surface_changed.Set(); + } + +-void Renderer::ResizeSurface() ++void Renderer::ResizeSurface(int new_width, int new_height) + { + std::lock_guard lock(m_swap_mutex); ++ m_new_surface_width = new_width; ++ m_new_surface_height = new_height; + m_surface_resized.Set(); + } + +diff -rupN dolphin.orig/Source/Core/VideoCommon/RenderBase.h dolphin/Source/Core/VideoCommon/RenderBase.h +--- dolphin.orig/Source/Core/VideoCommon/RenderBase.h 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoCommon/RenderBase.h 2024-03-05 04:06:08.240295843 +0000 +@@ -193,7 +193,8 @@ public: + std::tuple, MathUtil::Rectangle> + ConvertStereoRectangle(const MathUtil::Rectangle& rc) const; + +- unsigned int GetEFBScale() const; ++ bool IsUnscaled() const; ++ float GetEFBScalef() const; + + // Use this to upscale native EFB coordinates to IDEAL internal resolution + int EFBToScaledX(int x) const; +@@ -246,8 +247,8 @@ public: + VideoCommon::PostProcessing* GetPostProcessor() const { return m_post_processor.get(); } + // Final surface changing + // This is called when the surface is resized (WX) or the window changes (Android). +- void ChangeSurface(void* new_surface_handle); +- void ResizeSurface(); ++ void ChangeSurface(void* new_surface_handle, int new_width, int new_height); ++ void ResizeSurface(int new_width, int new_height); + bool UseVertexDepthRange() const; + void DoState(PointerWrap& p); + +@@ -342,6 +343,8 @@ protected: + std::unique_ptr m_post_processor; + + void* m_new_surface_handle = nullptr; ++ int m_new_surface_width = 0; ++ int m_new_surface_height = 0; + Common::Flag m_surface_changed; + Common::Flag m_surface_resized; + std::mutex m_swap_mutex; +@@ -357,7 +360,8 @@ private: + std::tuple CalculateOutputDimensions(int width, int height) const; + + PixelFormat m_prev_efb_format = PixelFormat::INVALID_FMT; +- unsigned int m_efb_scale = 1; ++ // Scale in steps of 0.5x. Value of 2 is scale of 1 (unscaled). ++ unsigned int m_efb_scale = 2; + + // These will be set on the first call to SetWindowSize. + int m_last_window_request_width = 0; +diff -rupN dolphin.orig/Source/Core/VideoCommon/TextureCacheBase.cpp dolphin/Source/Core/VideoCommon/TextureCacheBase.cpp +--- dolphin.orig/Source/Core/VideoCommon/TextureCacheBase.cpp 2024-03-05 04:05:26.602983293 +0000 ++++ dolphin/Source/Core/VideoCommon/TextureCacheBase.cpp 2024-03-05 04:06:08.240295843 +0000 +@@ -1046,7 +1046,7 @@ static void SetSamplerState(u32 index, f + // that have arbitrary contents, eg. are used for fog effects where the + // distance they kick in at is important to preserve at any resolution. + // Correct this with the upscaling factor of custom textures. +- s32 lod_offset = std::log2(g_renderer->GetEFBScale() / custom_tex_scale) * 256.f; ++ s64 lod_offset = std::log2(g_renderer->GetEFBScalef() / custom_tex_scale) * (s64) 256.f; + state.tm0.lod_bias = std::clamp(state.tm0.lod_bias + lod_offset, -32768, 32767); + + // Anisotropic also pushes mips farther away so it cannot be used either +@@ -2221,7 +2221,7 @@ void TextureCacheBase::CopyRenderTargetT + // TODO: This only produces perfect downsampling for 2x IR, other resolutions will need more + // complex down filtering to average all pixels and produce the correct result. + const bool linear_filter = +- !is_depth_copy && (scaleByHalf || g_renderer->GetEFBScale() != 1 || y_scale > 1.0f); ++ !is_depth_copy && (scaleByHalf || g_renderer->IsUnscaled() || y_scale > 1.0f); + + TCacheEntry* entry = nullptr; + if (copy_to_vram) diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/001-padorder.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/001-padorder.patch new file mode 100644 index 00000000000..187a57efcae --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/001-padorder.patch @@ -0,0 +1,111 @@ +diff --git a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp +index 6df37b4..64ade4a 100644 +--- a/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp ++++ b/Source/Core/InputCommon/ControllerInterface/evdev/evdev.cpp +@@ -392,6 +392,65 @@ void Init() + StartHotplugThread(); + } + ++struct joypad_udev_entry ++{ ++ const char *devnode; ++ struct udev_device *dev; ++}; ++ ++int isNumber(const char *s) { ++ int n; ++ ++ if(strlen(s) == 0) { ++ return 0; ++ } ++ ++ for(n=0; ndevnode, bb->devnode); ++} ++ + // Only call this when ControllerInterface::m_devices_population_mutex is locked + void PopulateDevices() + { +@@ -404,6 +463,10 @@ void PopulateDevices() + // We use udev to iterate over all /dev/input/event* devices. + // Note: the Linux kernel is currently limited to just 32 event devices. If + // this ever changes, hopefully udev will take care of this. ++ unsigned sorted_count = 0; ++ struct joypad_udev_entry sorted[64]; ++ const char* devnode; ++ int i; + + udev* const udev = udev_new(); + ASSERT_MSG(CONTROLLERINTERFACE, udev != nullptr, "Couldn't initialize libudev."); +@@ -422,11 +485,25 @@ void PopulateDevices() + + udev_device* dev = udev_device_new_from_syspath(udev, path); + +- if (const char* devnode = udev_device_get_devnode(dev)) +- AddDeviceNode(devnode); +- +- udev_device_unref(dev); ++ devnode = udev_device_get_devnode(dev); ++ if (devnode != NULL && sorted_count < 64) { ++ sorted[sorted_count].devnode = devnode; ++ sorted[sorted_count].dev = dev; ++ sorted_count++; ++ } else { ++ udev_device_unref(dev); ++ } + } ++ ++ /* Sort the udev entries by devnode name so that they are ++ * created in the proper order */ ++ qsort(sorted, sorted_count, sizeof(struct joypad_udev_entry), sort_devnodes); ++ ++ for (i = 0; i < sorted_count; i++) { ++ AddDeviceNode(sorted[i].devnode); ++ udev_device_unref(sorted[i].dev); ++ } ++ + udev_enumerate_unref(enumerate); + udev_unref(udev); + } diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/002-fix-bba.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/002-fix-bba.patch new file mode 100644 index 00000000000..b9fb92b1cbf --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/002-fix-bba.patch @@ -0,0 +1,46 @@ +From beebe0a2000826de66c911344c7efca4872262ff Mon Sep 17 00:00:00 2001 +From: "Admiral H. Curtiss" +Date: Wed, 4 Oct 2023 16:13:16 +0200 +Subject: [PATCH] Core/BBA/BuiltIn: Replace IP_PROTOCOL with IPV4_ETHERTYPE to + avoid conflict with Linux headers. + +--- + Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp | 3 ++- + Source/Core/Core/HW/EXI/BBA/BuiltIn.h | 3 --- + 2 files changed, 2 insertions(+), 4 deletions(-) + +diff --git a/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp b/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp +index 6e5c3eac9749..4782a0a8b0a7 100644 +--- a/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp ++++ b/Source/Core/Core/HW/EXI/BBA/BuiltIn.cpp +@@ -13,6 +13,7 @@ + #include "Common/BitUtils.h" + #include "Common/Logging/Log.h" + #include "Common/MsgHandler.h" ++#include "Common/Network.h" + #include "Common/ScopeGuard.h" + #include "Core/HW/EXI/EXI_Device.h" + #include "Core/HW/EXI/EXI_DeviceEthernet.h" +@@ -746,7 +747,7 @@ void CEXIETHERNET::BuiltInBBAInterface::ReadThreadHandler(CEXIETHERNET::BuiltInB + u8* buffer = reinterpret_cast(self->m_eth_ref->mRecvBuffer.get()); + Common::PacketView packet(buffer, datasize); + const auto packet_type = packet.GetEtherType(); +- if (packet_type.has_value() && packet_type == IP_PROTOCOL) ++ if (packet_type.has_value() && packet_type == Common::IPV4_ETHERTYPE) + { + SetIPIdentification(buffer, datasize, ++self->m_ip_frame_id); + } +diff --git a/Source/Core/Core/HW/EXI/BBA/BuiltIn.h b/Source/Core/Core/HW/EXI/BBA/BuiltIn.h +index 4478f4de48da..afcf39d2931b 100644 +--- a/Source/Core/Core/HW/EXI/BBA/BuiltIn.h ++++ b/Source/Core/Core/HW/EXI/BBA/BuiltIn.h +@@ -20,9 +20,6 @@ constexpr u16 TCP_FLAG_PSH = 0x8; + constexpr u16 TCP_FLAG_FIN = 0x1; + constexpr u16 TCP_FLAG_RST = 0x4; + +-constexpr u16 IP_PROTOCOL = 0x800; +-constexpr u16 ARP_PROTOCOL = 0x806; +- + constexpr u8 MAX_TCP_BUFFER = 4; + constexpr u16 MAX_UDP_LENGTH = 1500; + constexpr u16 MAX_TCP_LENGTH = 440; diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/004-git.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/004-git.patch new file mode 100644 index 00000000000..cbc4b582d12 --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/004-git.patch @@ -0,0 +1,13 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index eb0f83f..9d41166 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -128,7 +128,7 @@ endif() + include(CCache) + + # for revision info +-find_package(Git) ++#find_package(Git) + if(GIT_FOUND) + # make sure version information gets re-run when the current Git HEAD changes + execute_process(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/005-hide-osd-msg.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/005-hide-osd-msg.patch new file mode 100644 index 00000000000..50053098378 --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/005-hide-osd-msg.patch @@ -0,0 +1,13 @@ +--- a/Source/Core/VideoBackends/OGL/OGLRender.cpp 2020-12-31 00:27:53.998709857 +0100 ++++ b/Source/Core/VideoBackends/OGL/OGLRender.cpp 2020-12-31 00:28:40.414557344 +0100 +@@ -736,10 +736,6 @@ + g_Config.VerifyValidity(); + UpdateActiveConfig(); + +- OSD::AddMessage(fmt::format("Video Info: {}, {}, {}", g_ogl_config.gl_vendor, +- g_ogl_config.gl_renderer, g_ogl_config.gl_version), +- 5000); +- + if (!g_ogl_config.bSupportsGLBufferStorage && !g_ogl_config.bSupportsGLPinnedMemory) + { + OSD::AddMessage(fmt::format("Your OpenGL driver does not support {}_buffer_storage.", diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/008-bios-dir.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/008-bios-dir.patch new file mode 100644 index 00000000000..d9be1c4668b --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/008-bios-dir.patch @@ -0,0 +1,35 @@ +diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp +index b21d15478..5b62db919 100644 +--- a/Source/Core/Core/Boot/Boot.cpp ++++ b/Source/Core/Core/Boot/Boot.cpp +@@ -423,7 +423,7 @@ bool CBoot::Load_BS2(Core::System& system, const std::string& boot_rom_filename) + known_ipl = true; + break; + default: +- PanicAlertFmtT("The IPL file is not a known good dump. (CRC32: {0:x})", ipl_hash); ++ known_ipl = true; + break; + } + +@@ -598,7 +598,7 @@ bool CBoot::BootUp(Core::System& system, std::unique_ptr boot) + if (!File::Exists(ipl.path)) + { + if (ipl.disc) +- PanicAlertFmtT("Cannot start the game, because the GC IPL could not be found."); ++ PanicAlertFmtT("Gamecube bios not found, please add your IPL.bin to /storage/roms/bios/GC/(USA/JAP/EUR)/."); + else + PanicAlertFmtT("Cannot find the GC IPL."); + return false; +diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp +index 0442347fdd..af148e79f9 100644 +--- a/Source/Core/Core/Config/MainSettings.cpp ++++ b/Source/Core/Core/Config/MainSettings.cpp +@@ -616,7 +616,7 @@ const char* GetDirectoryForRegion(DiscIO::Region region, RegionDirectoryStyle st + std::string GetBootROMPath(const std::string& region_directory) + { + const std::string path = +- File::GetUserPath(D_GCUSER_IDX) + DIR_SEP + region_directory + DIR_SEP GC_IPL; ++ std::string("/storage/roms/bios/GC/") + DIR_SEP + region_directory + DIR_SEP + GC_IPL; + if (!File::Exists(path)) + return File::GetSysDirectory() + GC_SYS_DIR + DIR_SEP + region_directory + DIR_SEP GC_IPL; + return path; From 9fe586f64ed31d5d543435fa2256d3c04d4e59ac Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:32:33 +0900 Subject: [PATCH 032/110] dolphin-emu: add CMAKE_POLICY_VERSION_MINIMUM for the older Wayland-commit tree Externals/enet's cmake_minimum_required() predates 3.5, which current CMake rejects outright rather than warning. ROCKNIX's own package.mk carries the identical flag for this exact commit - confirmed via a failed single-package test build (CMake Error at Externals/enet/ CMakeLists.txt:1) before adding it here. --- package/batocera/emulators/dolphin-emu/dolphin-emu.mk | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index 0b4422dfec9..fe0afaf31ac 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -59,6 +59,11 @@ ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) DOLPHIN_EMU_CONF_OPTS += -DENABLE_QT=OFF DOLPHIN_EMU_CONF_OPTS += -DENABLE_NOGUI=ON DOLPHIN_EMU_CONF_OPTS += -DENABLE_WAYLAND=ON +# This older source tree's bundled Externals (e.g. enet) declare +# cmake_minimum_required() below 3.5, which current CMake rejects outright +# rather than just warning (matches ROCKNIX's own package.mk, which carries +# the same flag for this exact commit). +DOLPHIN_EMU_CONF_OPTS += -DCMAKE_POLICY_VERSION_MINIMUM=3.5 else ifeq ($(BR2_PACKAGE_QT6),y) DOLPHIN_EMU_DEPENDENCIES += qt6base qt6svg From 473abd11372069fdd450c95225d83ff4887ffadb Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:37:38 +0900 Subject: [PATCH 033/110] dolphin-emu: fix fmt 12 compile-string API break on the Wayland-commit tree This 2023-era source unconditionally links buildroot's system fmt (no USE_SYSTEM_FMT knob exists at that commit - find_package(fmt 8) is unconditional), and buildroot's fmt package is 12.1.0. fmt >=10 dropped fmt::detail::is_compile_string, breaking MsgHandler.h's static_assert. Confirmed via a failed single-package build (error: 'is_compile_string' is not a member of 'fmt::v12::detail'). The assert is a compile-time-only misuse check (catches passing a runtime string where a compile-time format string is expected) with no effect on behavior, so skipping it under FMT_VERSION >= 100000 is safe rather than chasing fmt's internal API across versions. --- .../009-fmt12-compat.patch | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch new file mode 100644 index 00000000000..c322eb2965c --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch @@ -0,0 +1,30 @@ +--- a/Source/Core/Common/MsgHandler.h 2026-08-15 14:34:13.546271877 +0900 ++++ b/Source/Core/Common/MsgHandler.h 2026-08-15 14:34:13.546271877 +0900 +@@ -41,7 +41,12 @@ + static_assert(NumFields == sizeof...(args), + "Unexpected number of replacement fields in format string; did you pass too few or " + "too many arguments?"); +-#if FMT_VERSION >= 90000 ++#if FMT_VERSION >= 100000 ++ // fmt 10+ removed/renamed the compile-string trait this used to check; ++ // this was a compile-time-only lint (misuse of a runtime string where a ++ // compile-time format string is expected), not runtime behavior, so it's ++ // safe to skip rather than chase fmt's internal API across versions. ++#elif FMT_VERSION >= 90000 + static_assert(fmt::detail::is_compile_string::value); + #else + static_assert(fmt::is_compile_string::value); +@@ -60,7 +65,12 @@ + static_assert(NumFields == sizeof...(args), + "Unexpected number of replacement fields in format string; did you pass too few or " + "too many arguments?"); +-#if FMT_VERSION >= 90000 ++#if FMT_VERSION >= 100000 ++ // fmt 10+ removed/renamed the compile-string trait this used to check; ++ // this was a compile-time-only lint (misuse of a runtime string where a ++ // compile-time format string is expected), not runtime behavior, so it's ++ // safe to skip rather than chase fmt's internal API across versions. ++#elif FMT_VERSION >= 90000 + static_assert(fmt::detail::is_compile_string::value); + #else + static_assert(fmt::is_compile_string::value); From 907e0dc4b56ea21eb5476e8e6f6a1e098cb3bccc Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:40:53 +0900 Subject: [PATCH 034/110] dolphin-emu: force the bundled Externals/fmt instead of patching for fmt 12 Revert 473abd1137's static_assert skip - it was treating a symptom and didn't even fully work (a second, deeper break surfaced next: ImGui's assert macros pass a compile-time format-string wrapper that no longer implicitly converts to fmt::string_view under fmt 12, unrelated to the static_assert). The actual mismatch is that this 2023-era source was written against fmt 9.1.0, which it already vendors at Externals/fmt - CMakeLists.txt already has a find_package(fmt 8)-else-vendored fallback, it's just that this board's shared system fmt (12.1.0) satisfies find_package first. -DCMAKE_DISABLE_FIND_PACKAGE_fmt=TRUE is CMake's standard mechanism for forcing that fallback branch, so the code builds against the exact fmt version it was actually written for instead of us chasing fmt's internal compile-time-format-string API through 3 major version bumps. --- .../emulators/dolphin-emu/dolphin-emu.mk | 8 +++++ .../009-fmt12-compat.patch | 30 ------------------- 2 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index fe0afaf31ac..b72de915088 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -64,6 +64,14 @@ DOLPHIN_EMU_CONF_OPTS += -DENABLE_WAYLAND=ON # rather than just warning (matches ROCKNIX's own package.mk, which carries # the same flag for this exact commit). DOLPHIN_EMU_CONF_OPTS += -DCMAKE_POLICY_VERSION_MINIMUM=3.5 +# This source predates fmt's compile-time format-string API overhaul (it +# was written against the fmt 9.1.0 it bundles in Externals/fmt) and fails +# to build against this board's shared system fmt (12.1.0 - fmt::detail:: +# is_compile_string and the FMT_COMPILE_STRING conversion path it relies on +# are gone). CMakeLists.txt already falls back to the bundled Externals/fmt +# whenever find_package(fmt) doesn't find one, so force that fallback +# instead of chasing fmt's internal API across 3 major versions. +DOLPHIN_EMU_CONF_OPTS += -DCMAKE_DISABLE_FIND_PACKAGE_fmt=TRUE else ifeq ($(BR2_PACKAGE_QT6),y) DOLPHIN_EMU_DEPENDENCIES += qt6base qt6svg diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch deleted file mode 100644 index c322eb2965c..00000000000 --- a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-fmt12-compat.patch +++ /dev/null @@ -1,30 +0,0 @@ ---- a/Source/Core/Common/MsgHandler.h 2026-08-15 14:34:13.546271877 +0900 -+++ b/Source/Core/Common/MsgHandler.h 2026-08-15 14:34:13.546271877 +0900 -@@ -41,7 +41,12 @@ - static_assert(NumFields == sizeof...(args), - "Unexpected number of replacement fields in format string; did you pass too few or " - "too many arguments?"); --#if FMT_VERSION >= 90000 -+#if FMT_VERSION >= 100000 -+ // fmt 10+ removed/renamed the compile-string trait this used to check; -+ // this was a compile-time-only lint (misuse of a runtime string where a -+ // compile-time format string is expected), not runtime behavior, so it's -+ // safe to skip rather than chase fmt's internal API across versions. -+#elif FMT_VERSION >= 90000 - static_assert(fmt::detail::is_compile_string::value); - #else - static_assert(fmt::is_compile_string::value); -@@ -60,7 +65,12 @@ - static_assert(NumFields == sizeof...(args), - "Unexpected number of replacement fields in format string; did you pass too few or " - "too many arguments?"); --#if FMT_VERSION >= 90000 -+#if FMT_VERSION >= 100000 -+ // fmt 10+ removed/renamed the compile-string trait this used to check; -+ // this was a compile-time-only lint (misuse of a runtime string where a -+ // compile-time format string is expected), not runtime behavior, so it's -+ // safe to skip rather than chase fmt's internal API across versions. -+#elif FMT_VERSION >= 90000 - static_assert(fmt::detail::is_compile_string::value); - #else - static_assert(fmt::is_compile_string::value); From ebc4f86e9616491c5a566748fb2fd3216329235f Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:44:04 +0900 Subject: [PATCH 035/110] dolphin-emu: disable ENABLE_X11 on the Wayland build (no GLX on this board) Common/CMakeLists.txt's legacy elseif(ENABLE_X11 AND X11_FOUND) branch unconditionally compiles GL/GLInterface/GLX.cpp, which needs GL/glx.h - provided by full mesa3d/libGL, not the mesa3d-headers this blob-GPU board uses. Confirmed via a failed single-package build (fatal error: GL/glx.h: No such file or directory). We don't need X11 at all here: EGLWayland.cpp (from the Wayland patch) already covers EGL, and Vulkan goes through vulkan-wsi-layer's native Wayland surface support. --- .../batocera/emulators/dolphin-emu/dolphin-emu.mk | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index b72de915088..87311a92e3b 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -92,6 +92,18 @@ else DOLPHIN_EMU_CONF_OPTS += -DENABLE_X11=OFF endif +ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) +# Override the XORG7-based ENABLE_X11=ON above (last -D wins on the CMake +# command line): this board's blob GPU driver only ships mesa3d-headers +# (GLES/EGL/Vulkan), not full mesa3d/libGL, so there's no GL/glx.h - the +# legacy elseif(ENABLE_X11) GLX.cpp path in Common/CMakeLists.txt fails to +# even find the header (confirmed via a failed single-package build). We +# don't need X11/XWayland at all here: EGLWayland.cpp (added by the +# Wayland patch) already covers native EGL, and Vulkan goes through +# vulkan-wsi-layer's Wayland surface support directly. +DOLPHIN_EMU_CONF_OPTS += -DENABLE_X11=OFF +endif + ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN),y) DOLPHIN_EMU_CONF_OPTS += -DENABLE_VULKAN=ON else From 95880ef0e0df9d574b133f1ad5de46fcc5103f08 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 14:47:34 +0900 Subject: [PATCH 036/110] dolphin-emu: fix missing include in bundled VulkanMemoryAllocator Externals/VulkanMemoryAllocator/include/vk_mem_alloc.h uses snprintf without including - same class of stale missing-include issue ROCKNIX's own pre_configure_target() already patches around in this file (they inject cstdint/string), just a different missing header surfaced by this toolchain's libstdc++. Confirmed via a failed single-package build (snprintf' was not declared in this scope). --- .../emulators/dolphin-emu/dolphin-emu.mk | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index 87311a92e3b..f9a23163d31 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -117,7 +117,21 @@ define DOLPHIN_EMU_PRE_CONFIGURE_HOOK sed -i 's/set(DOLPHIN_VERSION_MINOR .*)/set(DOLPHIN_VERSION_MINOR "$(DOLPHIN_EMU_VERSION_MINOR)")/' \ $(@D)/CMake/ScmRevGen.cmake endef -DOLPHIN_EMU_PRE_CONFIGURE_HOOKS = DOLPHIN_EMU_PRE_CONFIGURE_HOOK +DOLPHIN_EMU_PRE_CONFIGURE_HOOKS += DOLPHIN_EMU_PRE_CONFIGURE_HOOK +endif + +ifeq ($(BR2_PACKAGE_BATOCERA_WAYLAND),y) +# Externals/VulkanMemoryAllocator's bundled header (as of this old commit) +# uses snprintf without including - this toolchain's stricter +# libstdc++ doesn't pull it in transitively like whatever ROCKNIX built +# against. Same class of missing-include fix as ROCKNIX's own +# pre_configure_target() sed (which patches this exact file for +# cstdint/string) - confirmed via a failed single-package build. +define DOLPHIN_EMU_FIX_VMA_HEADER + sed -i '1i #include ' \ + $(@D)/Externals/VulkanMemoryAllocator/include/vk_mem_alloc.h +endef +DOLPHIN_EMU_PRE_CONFIGURE_HOOKS += DOLPHIN_EMU_FIX_VMA_HEADER endif define DOLPHIN_EMU_INI From 92277180a89ca9db478fedb76b64f766f54af778 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 15:15:29 +0900 Subject: [PATCH 037/110] mali-g52-odroidm1: install a Vulkan ICD manifest (was missing entirely) The g13p0 .deb this package pulls peripheral files from ships no /usr/share/vulkan/icd.d/*.json at all. Without one, the Khronos loader finds zero drivers and every Vulkan app fails at vkCreateInstance with VK_ERROR_INCOMPATIBLE_DRIVER ("Found no drivers!" per VK_LOADER_DEBUG=all) - regardless of vulkan-loader/vulkan-wsi-layer being otherwise correctly configured. This had only ever been patched in live via manual scp during earlier live debugging sessions (this session's own root cause hunt for a standalone-Dolphin Vulkan failure turned out to just be this file missing after a reboot wiped the RAM-overlaid /usr) - never actually captured into the package before. --- .../gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk | 13 +++++++++++++ .../batocera/gpu/mali-g52-odroidm1/mali.icd.json | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 package/batocera/gpu/mali-g52-odroidm1/mali.icd.json diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk index 79d763b204a..5b40c2545c1 100644 --- a/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk +++ b/package/batocera/gpu/mali-g52-odroidm1/mali-g52-odroidm1.mk @@ -180,6 +180,19 @@ define MALI_G52_ODROIDM1_INSTALL_TARGET_CMDS # every process's global scope via ld.so.preload (confirmed fix, # tested live via LD_PRELOAD) sidesteps whatever the exact defect is. echo "/usr/lib/libmali.so.1.9.0" >> $(TARGET_DIR)/etc/ld.so.preload + # The g13p0 .deb this package's peripheral files come from ships no + # Vulkan ICD manifest at all (unlike the sibling mali-G52 package's + # source, which needed 001-fix-vulkan-icd-path.patch for one that DID + # exist) - without this, the Khronos loader reports "Found no + # drivers!" and every Vulkan app fails at vkCreateInstance with + # VK_ERROR_INCOMPATIBLE_DRIVER, regardless of vulkan-loader/ + # vulkan-wsi-layer being otherwise correctly set up. Confirmed via a + # live single-binary test (VK_LOADER_DEBUG=all showed the exact + # "Found no drivers!" message) - this had only ever been patched in + # live via manual scp during earlier debugging, never captured here. + mkdir -p $(TARGET_DIR)/usr/share/vulkan/icd.d + $(INSTALL) -D -m 0644 $(MALI_G52_ODROIDM1_PKGDIR)/mali.icd.json \ + $(TARGET_DIR)/usr/share/vulkan/icd.d/mali.json endef $(eval $(generic-package)) diff --git a/package/batocera/gpu/mali-g52-odroidm1/mali.icd.json b/package/batocera/gpu/mali-g52-odroidm1/mali.icd.json new file mode 100644 index 00000000000..c8d008976dd --- /dev/null +++ b/package/batocera/gpu/mali-g52-odroidm1/mali.icd.json @@ -0,0 +1,7 @@ +{ + "file_format_version": "1.0.0", + "ICD": { + "library_path": "/usr/lib/libmali.so.1", + "api_version": "1.3.0" + } +} From e1cf4039c28ea22c256fa3e597944690021ef738 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 15:15:29 +0900 Subject: [PATCH 038/110] dolphin-emu: roundtrip after the initial wl_surface commit before Vulkan PlatformWayland::Init() commits the freshly-created wl_surface (with its opaque region set) and returns immediately, without ever waiting for the compositor to process that commit - Vulkan surface creation then happens on a surface the compositor may not have round-tripped past yet. Working test app via vulkan-wsi-layer (RetroArch/PPSSPP) Wayland client code always does this wait; this NoGUI platform (from the ROCKNIX Wayland patch, a fairly minimal port) didn't. Untested hypothesis for the VK_ERROR_OUT_OF_HOST_MEMORY seen at vkCreateWaylandSurfaceKHR - next single-package rebuild will confirm or rule it out. --- .../009-wayland-roundtrip-before-vulkan.patch | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-wayland-roundtrip-before-vulkan.patch diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-wayland-roundtrip-before-vulkan.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-wayland-roundtrip-before-vulkan.patch new file mode 100644 index 00000000000..5811097a7e4 --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/009-wayland-roundtrip-before-vulkan.patch @@ -0,0 +1,15 @@ +--- a/Source/Core/DolphinNoGUI/PlatformWayland.cpp 2026-08-15 15:14:45.990585353 +0900 ++++ b/Source/Core/DolphinNoGUI/PlatformWayland.cpp 2026-08-15 15:14:45.990585353 +0900 +@@ -198,6 +198,12 @@ + wl_surface_set_opaque_region(m_surface, m_region); + wl_surface_commit(m_surface); + ++ // Wait for the compositor to process the initial commit (xdg_surface's ++ // first configure event) before handing this surface to Vulkan - some ++ // WSI implementations query surface/output state that isn't valid until ++ // the compositor has round-tripped at least once past the initial commit. ++ wl_display_roundtrip(m_display); ++ + // This doesn't seem to have any effect on kwin... + xdg_surface_set_window_geometry(m_xdg_surface, Config::Get(Config::MAIN_RENDER_WINDOW_XPOS), + Config::Get(Config::MAIN_RENDER_WINDOW_YPOS), From 9df64959ee0c5874cc86de18eb47bc29343edda4 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 15:34:22 +0900 Subject: [PATCH 039/110] vulkan-wsi-layer: roundtrip before surface init - fixes intermittent VK_ERROR_OUT_OF_HOST_MEMORY on Wayland surface creation Root-caused via targeted instrumentation (temporary fprintf calls at each failure point in CreateWaylandSurfaceKHR/surface::init(), since this was a Debug build with WSI_LOG_ERROR active but none of those fired - the failure was happening before any of the named checks, inside make_surface()'s own construction/init path). The actual finding: adding I/O-latency-inducing debug prints made the bug stop reproducing - a classic race condition. surface::init() opens its own wl_event_queue and does its own wl_display_roundtrip_queue to discover zwp_linux_dmabuf_v1/wp_presentation and query supported formats, but never waits for the client's own already-queued traffic (e.g. Dolphin's initial xdg_surface commit, made from a different thread than the one calling vkCreateWaylandSurfaceKHR - see Core.cpp's "CPU Thread separate" log) to be flushed/processed first. An explicit wl_display_roundtrip() right at the top of CreateWaylandSurfaceKHR reproduces the same effect deliberately instead of relying on printf timing - live-tested 15s+ sustained 80-93% CPU, zero dmesg GPU faults, zero errors, in place of the previous 100%-reproducible instant failure. This was invisible with every WSI client tested so far this session (PPSSPP standalone/libretro, RetroArch) because none of them create their Vulkan surface from a different thread than the one that set up the window - Dolphin's separate CPU/video thread split is what exposes the race. Given the race is in the shared wl_display connection rather than anything GameCube/Dolphin-specific, this could plausibly affect any multi-threaded Wayland Vulkan client on this WSI layer, not just Dolphin - worth keeping in mind for future emulators. --- .../0001-fall-back-to-implicit-sync.patch | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch index 9c81ea43ad9..f66c8436407 100644 --- a/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch @@ -107,24 +107,49 @@ diff --git a/wsi/wayland/surface_properties.cpp b/wsi/wayland/surface_properties index 31ddc68..96b7729 100644 --- a/wsi/wayland/surface_properties.cpp +++ b/wsi/wayland/surface_properties.cpp -@@ -89,7 +89,7 @@ VkResult surface_properties::get_surface_capabilities(VkPhysicalDevice physical_ - +@@ -28,6 +28,7 @@ + #define __STDC_VERSION__ 0 + #endif + ++#include + #include + #include + +@@ -89,7 +90,7 @@ VkResult surface_properties::get_surface_capabilities(VkPhysicalDevice physical_ + /* Composite alpha */ pSurfaceCapabilities->supportedCompositeAlpha = static_cast( - VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR | VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR); + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR | VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR | VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR); return VK_SUCCESS; } - -@@ -338,7 +338,7 @@ static bool check_wl_protocols(struct wl_display *display) + +@@ -338,7 +339,11 @@ static bool check_wl_protocols(struct wl_display *display) return false; } - + - return (supported.dmabuf && supported.explicit_sync); -+ return (supported.dmabuf /* && supported.explicit_sync */); ++ /* explicit_sync (zwp_linux_explicit_synchronization_v1) is optional: newer ++ * wlroots (0.20+) dropped it in favour of linux-drm-syncobj-v1 and never ++ * advertises it, but the dma-buf's own implicit kernel fence still keeps ++ * presentation correct without it - see surface::init(). */ ++ return supported.dmabuf; } - + VWL_VKAPI_CALL(VkBool32) +@@ -365,6 +370,12 @@ static bool check_wl_protocols(struct wl_display *display) + CreateWaylandSurfaceKHR(VkInstance instance, const VkWaylandSurfaceCreateInfoKHR *pCreateInfo, + const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) VWL_API_POST + { ++ /* Give any Wayland traffic the client already queued (e.g. the initial ++ * xdg_surface commit) a chance to be flushed/processed before we open ++ * our own event queue and start querying compositor state - without ++ * this, surface init intermittently fails (observed as ++ * VK_ERROR_OUT_OF_HOST_MEMORY here). */ ++ wl_display_roundtrip(pCreateInfo->display); + auto &instance_data = layer::instance_private_data::get(instance); + util::allocator allocator{ instance_data.get_allocator(), VK_SYSTEM_ALLOCATION_SCOPE_OBJECT, pAllocator }; + auto wsi_surface = surface::make_surface(allocator, pCreateInfo->display, pCreateInfo->surface); diff --git a/wsi/wayland/swapchain.cpp b/wsi/wayland/swapchain.cpp index 91cb23f..254fdbd 100644 --- a/wsi/wayland/swapchain.cpp From 835b0bc93fbc39322ae74af40fa68410bd356a9a Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 19:20:15 +0900 Subject: [PATCH 040/110] libretro-beetle-psx: exclude the HW/GLES3 renderer on mesa3d-headers boards Live-tested on ODROID-M1 Track C (g29p1 Mali blob): the HAVE_HW=1 GLES=1 GLES3=1 renderer path produces severe vertical-stripe framebuffer corruption even over plain GL - not a Vulkan-specific issue (confirmed by testing with video_driver=gl too). This HAVE_HW/GLES3 gating predates this session's work (existing upstream batocera logic for generic aarch64 boards with BATOCERA_VULKAN + BATOCERA_GLES3) - it had simply never been exercised on this board before BATOCERA_VULKAN was turned on this session, and a separate install-path bug (fixed earlier, commit for the mednafen_psx_hw_ libretro.so filename mismatch) was masking this rendering bug behind a hard install failure until now. Scoped the exclusion to MESA3D_HEADERS specifically (same pattern as the duckstation mesa3d exclusion earlier this session) rather than touching the shared aarch64 gating, since other real-Vulkan-driver ARM boards using this same code path aren't known to have this issue. PS1 is already well covered by swanstation on this board, so no functional loss - this core's software/base renderer was never in use here regardless (this board only ever had the HW variant fail to install until today, so there's no working baseline being removed). --- .../libretro/libretro-beetle-psx/libretro-beetle-psx.mk | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package/batocera/emulators/retroarch/libretro/libretro-beetle-psx/libretro-beetle-psx.mk b/package/batocera/emulators/retroarch/libretro/libretro-beetle-psx/libretro-beetle-psx.mk index fff982543a9..4d70f32845f 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-beetle-psx/libretro-beetle-psx.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-beetle-psx/libretro-beetle-psx.mk @@ -26,7 +26,14 @@ LIBRETRO_BEETLE_PSX_EXTRAOPT = LIBRETRO_BEETLE_PSX_OUTFILE = mednafen_psx_libretro.so # Batocera - SBC required_hw_api = "OpenGL Core >= 3.3 | Vulkan >= 1.0" +# Excluded on vendor-blob boards using mesa3d-headers (MESA3D_HEADERS, e.g. +# ODROID-M1 Track C's g29p1 Mali blob): live-tested and confirmed the HW/ +# GLES3 renderer produces severe vertical-stripe framebuffer corruption on +# this exact driver even over plain GL (not just Vulkan) - PS1 is already +# well covered by swanstation on this board, so no functional loss from +# excluding it here rather than chasing the underlying driver bug. ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN)$(BR2_PACKAGE_BATOCERA_GLES3),yy) +ifneq ($(BR2_PACKAGE_MESA3D_HEADERS),y) LIBRETRO_BEETLE_PSX_EXTRAOPT += HAVE_HW=1 LIBRETRO_BEETLE_PSX_OUTFILE = mednafen_psx_hw_libretro.so # Only force GLES/GLES3 flags if we are NOT on a x86 device @@ -34,6 +41,7 @@ ifeq ($(BR2_x86_64),) LIBRETRO_BEETLE_PSX_EXTRAOPT += GLES=1 GLES3=1 endif endif +endif define LIBRETRO_BEETLE_PSX_BUILD_CMDS $(TARGET_CONFIGURE_OPTS) $(MAKE) CXX="$(TARGET_CXX)" CC="$(TARGET_CC)" \ From 470be1ef4e7a75d7b774dfb9c956f701156e144b Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 19:20:48 +0900 Subject: [PATCH 041/110] board: correct stale parallel-n64 comment - it's GL-only on ARM, not Vulkan Confirmed live earlier this session (checked /proc/PID/maps: no libvulkan.so ever loads) and cross-checked against ROCKNIX's own libretro-parallel-n64-lr packaging, which gates ParaLLEl-RDP's Vulkan renderer to AMD64 only - matches our finding exactly. The comment had called it a "Vulkan-only renderer", which was never accurate on ARM. --- configs/batocera-rk3568-odroidm1.board | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index b7935052453..4168e3bced3 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -180,8 +180,13 @@ BR2_PACKAGE_PCSX2=y # (GL only). Has its own Vulkan gfxbackend option once g29p1 is in. BR2_PACKAGE_DOLPHIN_EMU=y -# ParaLLEl-RDP N64 core - Vulkan-only renderer, needs the RK3568 arch -# branch fix (see libretro-parallel-n64.mk commit) to cross-compile aarch64. +# parallel-n64 core, needs the RK3568 arch branch fix (see +# libretro-parallel-n64.mk commit) to cross-compile aarch64. NOTE: despite +# the core's name, its ParaLLEl-RDP Vulkan renderer is gated to AMD64 only +# upstream (confirmed live: /proc/PID/maps never loads libvulkan.so here, +# and cross-checked against ROCKNIX's own libretro packaging, which gates +# it the same way) - this core only ever runs its GL path on ARM. N64 is +# already well covered by mupen64plus-next (GLES) on this board instead. BR2_PACKAGE_LIBRETRO_PARALLEL_N64=y # libretro Azahar (3DS) - no 3DS system was buildable on this board before. From d4ef6fe62be3c821ec49099f3b4ebf0a91ad4f0c Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 22:26:49 +0900 Subject: [PATCH 042/110] dolphin-emu: fix standalone Wayland platform staying windowed/cropped ROCKNIX's Wayland backend (PlatformWayland.cpp) never actually applied Dolphin.ini's [Display] Fullscreen setting, and even once requested, the resulting swapchain ended up sized/positioned wrong. Three bugs, fixed together in 010-wayland-fullscreen.patch: - Config wasn't loaded yet when Init() ran (UICommon::Init(), which reads Dolphin.ini, only happens afterwards in MainNoGUI's main()), so the fullscreen check always saw the compiled-in default (false). Moved the request into a new Platform::RequestFullscreenIfNeeded() hook, called once UICommon::Init() has actually run. - xdg_surface_set_window_geometry() was only ever set once at startup to the small pre-fullscreen default and never resynced on resize, leaving the compositor clipping/positioning the surface to that stale small rect even after the toplevel grew to fullscreen size. Now kept in sync on every TopLevelConfigure. - MainNoGUI's main() captured GetWindowSystemInfo() (whose width/height is what BootManager::BootCore() uses to size the actual swapchain, once, for the whole session) *before* requesting fullscreen, so the swapchain was always created at the small pre-fullscreen size regardless of the above two fixes. Reordered to request fullscreen first, with a wl_display_roundtrip() to block until the compositor's real-size configure event has been processed, before capturing wsi. Verified live on real hardware: Pikmin (GameCube) now boots straight into a correctly centered, full 4:3 render with no cropping. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../010-wayland-fullscreen.patch | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/010-wayland-fullscreen.patch diff --git a/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/010-wayland-fullscreen.patch b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/010-wayland-fullscreen.patch new file mode 100644 index 00000000000..785d8045fae --- /dev/null +++ b/package/batocera/emulators/dolphin-emu/e6583f8bec814d8f3748f1d7738457600ce0de56/010-wayland-fullscreen.patch @@ -0,0 +1,104 @@ +--- a/Source/Core/DolphinNoGUI/PlatformWayland.cpp ++++ b/Source/Core/DolphinNoGUI/PlatformWayland.cpp +@@ -39,6 +39,7 @@ + bool Init() override; + void SetTitle(const std::string& string) override; + void MainLoop() override; ++ void RequestFullscreenIfNeeded() override; + + WindowSystemInfo GetWindowSystemInfo() const; + +@@ -135,6 +136,17 @@ + PlatformWayland* platform = static_cast(data); + platform->m_surface_width = width; + platform->m_surface_height = height; ++ ++ // Init() pinned window_geometry to the small startup size (and whatever ++ // xpos/ypos Dolphin defaults to) before fullscreen was ever requested. ++ // xdg-shell clients are expected to keep window_geometry in sync with ++ // their actual content size - leaving it stale after a resize (e.g. the ++ // transition to fullscreen) makes the compositor keep clipping/ ++ // positioning the surface to that old small rect, which is why the ++ // rendered content ended up cropped into a corner instead of covering ++ // the whole (now much bigger) surface. Keep it matched to the new size. ++ xdg_surface_set_window_geometry(platform->m_xdg_surface, 0, 0, width, height); ++ + if (g_renderer) + g_renderer->ResizeSurface(width, height); + //if (g_controller_interface.IsInit()) +@@ -227,6 +239,34 @@ + xdg_toplevel_set_title(m_xdg_toplevel, string.c_str()); + } + ++void PlatformWayland::RequestFullscreenIfNeeded() ++{ ++ // Batocera always writes [Display] Fullscreen into dolphin.ini (see ++ // configgen's dolphinGenerator.py), but Config isn't loaded yet during ++ // Init() - UICommon::Init() (which actually reads Dolphin.ini) only runs ++ // afterwards in MainNoGUI.cpp, so Config::Get() during Init() only ever ++ // saw the compiled-in default (false). This is called once main() has ++ // called UICommon::Init(), so the real ini value is available - request ++ // fullscreen here instead, which is still valid per xdg-shell even after ++ // the initial surface commit. ++ if (Config::Get(Config::MAIN_FULLSCREEN)) ++ { ++ xdg_toplevel_set_fullscreen(m_xdg_toplevel, nullptr); ++ ++ // The caller (MainNoGUI's main()) uses GetWindowSystemInfo() right ++ // after this call to size the swapchain for the whole session via ++ // BootManager::BootCore() - it never re-queries later. Block here ++ // until the compositor's resulting configure event (the one that ++ // updates m_surface_width/height to the real fullscreen size) has ++ // actually been processed, so that first swapchain is already ++ // correctly sized instead of relying on a subsequent ResizeSurface() ++ // call to fix it up (which silently no-ops via the `if (g_renderer)` ++ // check below if it fires before the renderer exists yet - it always ++ // does, this early in startup). ++ wl_display_roundtrip(m_display); ++ } ++} ++ + void PlatformWayland::MainLoop() + { + while (IsRunning()) +--- a/Source/Core/DolphinNoGUI/Platform.h ++++ b/Source/Core/DolphinNoGUI/Platform.h +@@ -22,6 +22,11 @@ + virtual void SetTitle(const std::string& title); + virtual void MainLoop() = 0; + ++ // Called once after UICommon::Init() (i.e. once the ini-file config is ++ // actually loaded) so platforms can apply settings - like [Display] ++ // Fullscreen - that weren't available yet during Init() itself. ++ virtual void RequestFullscreenIfNeeded() {} ++ + virtual WindowSystemInfo GetWindowSystemInfo() const = 0; + + // Requests a graceful shutdown, from SIGINT/SIGTERM. +--- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp ++++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp +@@ -267,10 +267,22 @@ + return 1; + } + +- const WindowSystemInfo wsi = s_platform->GetWindowSystemInfo(); +- + UICommon::SetUserDirectory(user_directory); + UICommon::Init(); ++ ++ // Dolphin.ini's [Display] Fullscreen is only actually loaded once ++ // UICommon::Init() has run - platforms that need it (Wayland) apply it ++ // here rather than during Platform::Init(), which runs before this. ++ // This must also happen *before* GetWindowSystemInfo() below: that call's ++ // result is what BootManager::BootCore() uses to size the actual ++ // swapchain, and on Wayland it only reflects the real (fullscreen) size ++ // if the compositor's resulting configure event has already been ++ // processed - otherwise every boot silently creates the swapchain at ++ // whatever the small pre-fullscreen default was. ++ s_platform->RequestFullscreenIfNeeded(); ++ ++ const WindowSystemInfo wsi = s_platform->GetWindowSystemInfo(); ++ + UICommon::InitControllers(wsi); + + Common::ScopeGuard ui_common_guard([] { From 3a5bce9d03c5206eb000913a9d53b86ab3dc8bdc Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sat, 15 Aug 2026 23:44:56 +0900 Subject: [PATCH 043/110] labwc: force pcsx2-qt fullscreen - fixes zero-size window on launch PCSX2's own Qt6 QPA Wayland plugin was receiving an xdg_toplevel.configure event with QSize(0, 0) (the compositor telling the client "you choose the size", per xdg-shell) and applying it verbatim instead of falling back to a sane default - confirmed live via QT_LOGGING_RULES="qt.qpa.*=true": "Applied pending xdg_toplevel configure event: QSize(0, 0)", repeating forever with no recovery. Result: process runs (BIOS boots, CPU busy) but nothing ever gets composited, since the window has zero area - matches the previously reported "runs but never displays" symptom exactly. This is the same class of issue this project's labwc config already works around for several other windowed apps (x16emu, pcmanfm, clksignal, retrotrivia.pygame, CGeniusExe) via a windowRule forcing ToggleFullscreen, which makes the compositor assign a definite size outright instead of going through the broken negotiation. Applying the same pattern to the existing (but so far decoration-only) pcsx2-qt windowRule fixes it: verified live (rc.xml hot-reloaded into a running labwc via SIGHUP, no restart needed) - the PCSX2 first-run setup wizard now renders fully and correctly instead of a blank/ zero-size window. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../batocera-emulationstation/wayland/labwc/rc.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml index 1af62e55b6d..7b382342ff7 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml +++ b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml @@ -140,6 +140,9 @@ + + + From fb3b104224eb4711c40681d55488a33fd04d4391 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sun, 16 Aug 2026 10:15:24 +0900 Subject: [PATCH 044/110] labwc: give pcsx2-qt keyboard focus alongside ToggleFullscreen ToggleFullscreen alone (previous commit 3a5bce9d03) fixed the zero-size window, but the window never actually received keyboard focus - Qt's first-run setup wizard rendered fine but ignored all keyboard input. Verified live (rc.xml hot-reloaded via SIGHUP): with Focus added, the wizard responds to Tab/Shift+Tab navigation and Enter as expected - confirmed by walking a real first-run wizard end to end (Language, BIOS Image, Game Directories, Controller Setup, RetroAchievements, Complete) with a physical keyboard. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../batocera-emulationstation/wayland/labwc/rc.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml index 7b382342ff7..f0e3085cd4e 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml +++ b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml @@ -142,6 +142,7 @@ + From cd2cced1069af139fa66a6152417cbe6fe6c938e Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sun, 16 Aug 2026 11:10:48 +0900 Subject: [PATCH 045/110] pcsx2: fix Vulkan swap chain double-free + retry surface format race VKSwapChain::Create() transfers ownership of the VkSurfaceKHR into the VKSwapChain it constructs and, on failure, the temporary object's destructor already destroys that surface before Create() returns null. GSDeviceVK::CreateDeviceAndSwapChain()'s ScopedGuard didn't know this and destroyed the same (already-freed) handle again on the failure path, causing a genuine double-free / heap corruption crash. Clear the local surface handle right after the Create() call so the guard becomes a no-op once ownership has transferred either way. Separately, vkGetPhysicalDeviceSurfaceFormatsKHR() can transiently report zero formats (VK_SUCCESS, format_count == 0) right after window creation or a compositor-driven fullscreen toggle on Wayland, before labwc finishes the configure handshake. This was being treated as a hard failure, which fed straight into the double-free above. Retry SelectSurfaceFormat for up to ~200ms before giving up. Together these fix the PS2 boot crash: GS device lost recovery (which reliably happens ~13s into every boot on this board, root cause still open) used to crash the whole process; now it recreates the swap chain and the game keeps running. Verified stable across 5 consecutive boots plus one extended run with no crash. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- .../pcsx2/017-fix-swapchain-double-free.patch | 29 +++++++++++++ .../pcsx2/018-retry-surface-format-race.patch | 43 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 package/batocera/emulators/pcsx2/017-fix-swapchain-double-free.patch create mode 100644 package/batocera/emulators/pcsx2/018-retry-surface-format-race.patch diff --git a/package/batocera/emulators/pcsx2/017-fix-swapchain-double-free.patch b/package/batocera/emulators/pcsx2/017-fix-swapchain-double-free.patch new file mode 100644 index 00000000000..70e1b0e86ba --- /dev/null +++ b/package/batocera/emulators/pcsx2/017-fix-swapchain-double-free.patch @@ -0,0 +1,29 @@ +--- pcsx2-v2.4.0/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp ++++ pcsx2-v2.4.0-lbrpdx/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +@@ -2789,9 +2789,23 @@ + if (surface != VK_NULL_HANDLE) + { + VkPresentModeKHR present_mode; +- if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode) || +- !(m_swap_chain = VKSwapChain::Create(m_window_info, surface, present_mode, +- Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)))) ++ if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode)) ++ { ++ ERROR_LOG("Failed to create swap chain"); ++ return false; ++ } ++ ++ m_swap_chain = VKSwapChain::Create(m_window_info, surface, present_mode, ++ Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)); ++ ++ // VKSwapChain::Create() takes ownership of surface as soon as it constructs the ++ // VKSwapChain (m_surface(surface) in the ctor init list), even when CreateSwapChain() ++ // subsequently fails and Create() returns null - the temporary VKSwapChain's destructor ++ // already destroyed the surface in that case. surface_cleanup below must not destroy it ++ // a second time, so clear the local handle unconditionally right after this call. ++ surface = VK_NULL_HANDLE; ++ ++ if (!m_swap_chain) + { + ERROR_LOG("Failed to create swap chain"); + return false; diff --git a/package/batocera/emulators/pcsx2/018-retry-surface-format-race.patch b/package/batocera/emulators/pcsx2/018-retry-surface-format-race.patch new file mode 100644 index 00000000000..26047c4b32b --- /dev/null +++ b/package/batocera/emulators/pcsx2/018-retry-surface-format-race.patch @@ -0,0 +1,43 @@ +--- pcsx2-v2.4.0/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp ++++ pcsx2-v2.4.0-lbrpdx/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp +@@ -9,6 +9,7 @@ + #include "common/Assertions.h" + #include "common/CocoaTools.h" + #include "common/Console.h" ++#include "common/Threading.h" + #include "common/Timer.h" + + #include +@@ -339,9 +340,29 @@ + + std::optional VKSwapChain::SelectSurfaceFormat(VkSurfaceKHR surface) + { +- u32 format_count; +- VkResult res = vkGetPhysicalDeviceSurfaceFormatsKHR( +- GSDeviceVK::GetInstance()->GetPhysicalDevice(), surface, &format_count, nullptr); ++ // The Wayland surface configure handshake is asynchronous: right after window ++ // creation (or a compositor-driven fullscreen toggle), the surface can briefly ++ // report zero formats even though vkGetPhysicalDeviceSurfaceFormatsKHR() itself ++ // succeeds (VK_SUCCESS with format_count == 0), before the compositor finishes ++ // acking the configure. This is transient, not a real driver/device failure, so ++ // retry for a short while instead of failing the swap chain outright (which was ++ // leaving the caller to tear down a partially-constructed GSDeviceVK and hit ++ // heap corruption in that failure path). ++ u32 format_count = 0; ++ VkResult res = VK_SUCCESS; ++ for (int attempt = 0; attempt < 20; attempt++) ++ { ++ format_count = 0; ++ res = vkGetPhysicalDeviceSurfaceFormatsKHR( ++ GSDeviceVK::GetInstance()->GetPhysicalDevice(), surface, &format_count, nullptr); ++ if (res == VK_SUCCESS && format_count > 0) ++ break; ++ ++ if (res != VK_SUCCESS) ++ break; ++ ++ Threading::Sleep(10); ++ } + if (res != VK_SUCCESS || format_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceSurfaceFormatsKHR failed: "); From c11e3959d9f2953ff7036ca9467735e6861cbe48 Mon Sep 17 00:00:00 2001 From: "Jang,Pyohwan" Date: Sun, 16 Aug 2026 15:15:45 +0900 Subject: [PATCH 046/110] ps2: add AetherSX2 as a personal-build PS2 core alongside pcsx2 Vanilla PCSX2/bmdhacks-derived GS renderers hit a real Mali G52 GPU hardware fault (DATA_INVALID_FAULT, job status 0x58) a few seconds to a couple minutes into real gameplay on this g29p1 blob + vendor kbase g25p0 combination (see project_odroid_retro.md 2026-08-16). ROCKNIX ships AetherSX2 - a prebuilt aarch64 binary with an older GS renderer branch (Tahlreth's Android-era Adreno/Mali-hardened command patterns) - as the default PS2 core on RK3566, a nearly identical Mali stack. Live smoke-tested on this board for 5+ minutes of actual gameplay with zero GPU faults where bmdhacks/pcsx2 reliably faults within ~100s, then verified again end-to-end through the real ES/configgen launch path (not just a manual SSH invocation). New package (package/batocera/emulators/aethersx2) fetches ROCKNIX's aethersx2.tar.gz (which, despite the "AppImage" framing, is actually a plain aarch64 ELF linked against system Qt6 - confirmed via ELF magic bytes) and installs it to /usr/aethersx2. It link-time-depends on libGLX/libOpenGL/libGLdispatch/libGLX_mesa/libpcap.so.0.8, which this board doesn't otherwise ship (no desktop GL/GLX stack, blob-only Vulkan+GLES) - vendored as binaries pulled from a working Debian aarch64 install and live-tested here first, since the generator forces Renderer=14 (Vulkan) so real GLX is never actually invoked at runtime. Registered as a second "aethersx2" emulator on the existing ps2 es_systems.yml entry (pcsx2 stays as-is), with a new aethersx2Generator.py configgen generator and a labwc rc.xml Focus rule (title-matched, since the app_id wasn't confirmed) - fullscreen already works out of the box via the binary's own -fullscreen flag, so no ToggleFullscreen hack was needed unlike pcsx2-qt. Personal-build only: AetherSX2 can't go into Batocera upstream due to its past GPL dispute history (bmdhacks was adopted there specifically as the replacement for that reason). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01F4zVPhdz6gmjHPNpGGTQei --- Config.in | 1 + .../generators/aethersx2/__init__.py | 0 .../aethersx2/aethersx2Generator.py | 95 ++++++++++++++++++ .../wayland/labwc/rc.xml | 3 + .../batocera-es-system/es_systems.yml | 2 + .../batocera/emulators/aethersx2/Config.in | 22 ++++ .../aethersx2/aethersx2.emulator.yml | 16 +++ .../batocera/emulators/aethersx2/aethersx2.mk | 46 +++++++++ .../emulators/aethersx2/libs/libGL.so.1 | Bin 0 -> 936040 bytes .../emulators/aethersx2/libs/libGLX.so.0 | Bin 0 -> 133480 bytes .../emulators/aethersx2/libs/libGLX_mesa.so.0 | Bin 0 -> 533016 bytes .../aethersx2/libs/libGLdispatch.so.0 | Bin 0 -> 1517784 bytes .../emulators/aethersx2/libs/libOpenGL.so.0 | Bin 0 -> 370784 bytes .../emulators/aethersx2/libs/libpcap.so.0.8 | Bin 0 -> 285752 bytes 14 files changed, 185 insertions(+) create mode 100644 package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/__init__.py create mode 100644 package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py create mode 100644 package/batocera/emulators/aethersx2/Config.in create mode 100644 package/batocera/emulators/aethersx2/aethersx2.emulator.yml create mode 100644 package/batocera/emulators/aethersx2/aethersx2.mk create mode 100644 package/batocera/emulators/aethersx2/libs/libGL.so.1 create mode 100644 package/batocera/emulators/aethersx2/libs/libGLX.so.0 create mode 100644 package/batocera/emulators/aethersx2/libs/libGLX_mesa.so.0 create mode 100644 package/batocera/emulators/aethersx2/libs/libGLdispatch.so.0 create mode 100644 package/batocera/emulators/aethersx2/libs/libOpenGL.so.0 create mode 100644 package/batocera/emulators/aethersx2/libs/libpcap.so.0.8 diff --git a/Config.in b/Config.in index 085af43d6d8..827e16150d5 100644 --- a/Config.in +++ b/Config.in @@ -382,6 +382,7 @@ menu "Emulators" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/xenia-canary/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/xenia-edge/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/pcsx2/Config.in" + source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/aethersx2/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/rpcs3/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/ppsspp/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/flycast/Config.in" diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/__init__.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py new file mode 100644 index 00000000000..e674feffe47 --- /dev/null +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Final + +from batocera_common.configparser import CaseSensitiveConfigParser + +from ... import Command +from ...batoceraPaths import BIOS, HOME, mkdir_if_not_exists +from ...controller import generate_sdl_game_controller_config +from ..Generator import Generator + +if TYPE_CHECKING: + from collections.abc import Mapping + + from ...config import SystemConfig + from ...Emulator import Emulator + from ...gun import Guns + from ...input import Input + from ...types import DeviceInfoMapping, HotkeysContext, Resolution + +_AETHERSX2_BIN: Final = Path("/usr/aethersx2/aethersx2") +_AETHERSX2_LIBS: Final = Path("/usr/aethersx2/libs") +# Not batocera's usual CONFIGS ('configs', no dot) - the binary resolves its +# own DataRoot Directory to $HOME/.config/aethersx2 by itself (confirmed via +# its own startup log during the 2026-08-16 smoke test), so we write the ini +# to wherever it actually looks rather than fighting that default. +_AETHERSX2_CONFIG: Final = HOME / ".config" / "aethersx2" +_AETHERSX2_BIOS: Final = BIOS / "ps2" + + +class Aethersx2Generator(Generator): + + def getHotkeysContext(self) -> HotkeysContext: + return { + "name": "aethersx2", + "keys": { + "exit": ["KEY_LEFTALT", "KEY_F4"], + "menu": "KEY_ESC", + "pause": "KEY_ESC", + } + } + + def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution): + configureINI(_AETHERSX2_CONFIG, system) + linkBios(_AETHERSX2_CONFIG / "bios") + + commandArray = [str(_AETHERSX2_BIN), "-bigpicture", "-fullscreen", str(rom)] + + envcmd: dict[str, str] = { + "QT_QPA_PLATFORM": "wayland", + "QT_PLUGIN_PATH": "/usr/lib/qt6/plugins", + "LD_LIBRARY_PATH": f"{_AETHERSX2_LIBS}:/usr/lib", + "SDL_GAMECONTROLLERCONFIG": generate_sdl_game_controller_config(playersControllers), + } + + return Command.Command(array=commandArray, env=envcmd) + + +def linkBios(bios_dir: Path) -> None: + mkdir_if_not_exists(bios_dir) + if not _AETHERSX2_BIOS.is_dir(): + return + for f in _AETHERSX2_BIOS.glob("*.bin"): + dest = bios_dir / f.name + if not dest.exists(): + dest.symlink_to(f) + + +def configureINI(config_directory: Path, system: Emulator) -> None: + ini_file = config_directory / "inis" / "PCSX2.ini" + mkdir_if_not_exists(ini_file.parent) + + cfg = CaseSensitiveConfigParser(interpolation=None) + if ini_file.is_file(): + cfg.read(ini_file) + + if not cfg.has_section("UI"): + cfg.add_section("UI") + cfg.set("UI", "StartFullscreen", "true") + cfg.set("UI", "ConfirmShutdown", "false") + cfg.set("UI", "HideMouseCursor", "true") + + if not cfg.has_section("EmuCore/GS"): + cfg.add_section("EmuCore/GS") + # Auto (-1) tries OpenGL first on this fork, which fails outright in our + # headless/Wayland-only environment (no real GLX acceleration) - force + # Vulkan, matching both ROCKNIX's own RK3566 default and the exact + # config this was smoke-tested with (see project_odroid_retro.md, + # 2026-08-16 AetherSX2 section). + cfg.set("EmuCore/GS", "Renderer", "14") + cfg.set("EmuCore/GS", "upscale_multiplier", system.config.get("aethersx2_resolution", "1")) + + with ini_file.open("w") as f: + cfg.write(f) diff --git a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml index f0e3085cd4e..08bc79d1af6 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml +++ b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml @@ -144,6 +144,9 @@ + + + diff --git a/package/batocera/emulationstation/batocera-es-system/es_systems.yml b/package/batocera/emulationstation/batocera-es-system/es_systems.yml index 4b6b76e834b..5302aa8ff89 100644 --- a/package/batocera/emulationstation/batocera-es-system/es_systems.yml +++ b/package/batocera/emulationstation/batocera-es-system/es_systems.yml @@ -2288,6 +2288,8 @@ ps2: hardware: console extensions: [iso, mdf, nrg, bin, img, dump, gz, cso, chd, m3u, zso] emulators: + aethersx2: + aethersx2: { requireAnyOf: [BR2_PACKAGE_AETHERSX2], incompatible_extensions: [m3u] } pcsx2: pcsx2: { requireAnyOf: [BR2_PACKAGE_PCSX2], incompatible_extensions: [m3u] } libretro: diff --git a/package/batocera/emulators/aethersx2/Config.in b/package/batocera/emulators/aethersx2/Config.in new file mode 100644 index 00000000000..6f027dfbf17 --- /dev/null +++ b/package/batocera/emulators/aethersx2/Config.in @@ -0,0 +1,22 @@ +config BR2_PACKAGE_AETHERSX2 + bool "aethersx2" + depends on BR2_aarch64 + select BR2_PACKAGE_BATOCERA_QT6 + select BR2_PACKAGE_LIBGPG_ERROR + select BR2_PACKAGE_LIBPCAP + select BR2_PACKAGE_LIBCURL + help + AetherSX2 is a prebuilt aarch64 PlayStation 2 emulator binary, + vendored as-is from ROCKNIX (same one they ship as the default PS2 + core on RK3566 boards). Its GS renderer is an older branch (hardened + for Android-era Adreno/Mali by Tahlreth) that avoids the real Mali + G52 GPU hardware fault (DATA_INVALID_FAULT) that newer mainline + PCSX2-derived forks (including the bmdhacks fork this board's own + pcsx2 package uses) hit on this g29p1 blob - live-tested 5+ minutes + fault-free where bmdhacks/pcsx2 reliably faults within ~100s. + + Personal-build only: not upstream-contributable due to AetherSX2's + past GPL dispute history (bmdhacks was adopted upstream as the + replacement for exactly that reason). + + https://github.com/ROCKNIX/packages diff --git a/package/batocera/emulators/aethersx2/aethersx2.emulator.yml b/package/batocera/emulators/aethersx2/aethersx2.emulator.yml new file mode 100644 index 00000000000..17531232159 --- /dev/null +++ b/package/batocera/emulators/aethersx2/aethersx2.emulator.yml @@ -0,0 +1,16 @@ +cores: + aethersx2: + shared_features: + - powermode + - tdp + - videomode + - bezel + - bezel_stretch + - hud + - hud_corner + - use_guns + - use_wheels +systems: + - name: ps2 + exclude_extensions: + - m3u diff --git a/package/batocera/emulators/aethersx2/aethersx2.mk b/package/batocera/emulators/aethersx2/aethersx2.mk new file mode 100644 index 00000000000..3634db465fc --- /dev/null +++ b/package/batocera/emulators/aethersx2/aethersx2.mk @@ -0,0 +1,46 @@ +################################################################################ +# +# aethersx2 +# +################################################################################ +# Prebuilt aarch64 ELF binary (not an AppImage, despite the ROCKNIX package +# description calling it one - confirmed via \x7fELF magic bytes), vendored +# as-is from ROCKNIX. Vanilla PCSX2/bmdhacks-derived GS renderers hit a real +# Mali G52 GPU hardware fault (DATA_INVALID_FAULT, job status 0x58) a few +# seconds to a couple minutes into real gameplay on this exact g29p1 blob + +# vendor kbase g25p0 combination (see project_odroid_retro.md, 2026-08-16, +# for the full gdb/dmesg debugging writeup). AetherSX2 ships an older GS +# renderer branch (Tahlreth's Android-era Adreno/Mali-hardened command +# patterns) that does not hit it - live smoke-tested on this board for 5+ +# minutes of actual gameplay with zero GPU faults where bmdhacks/pcsx2 +# reliably faults within ~100s. This package exists purely to make that +# already-proven-working binary launchable from ES. +AETHERSX2_VERSION = 1.5-3606 +AETHERSX2_SOURCE = aethersx2.tar.gz +AETHERSX2_SITE = https://github.com/ROCKNIX/packages/raw/refs/heads/main +AETHERSX2_LICENSE = LGPL +AETHERSX2_EMULATOR_INFO = aethersx2.emulator.yml + +define AETHERSX2_EXTRACT_CMDS + mkdir -p $(@D) + $(TAR) xf $(AETHERSX2_DL_DIR)/$(AETHERSX2_SOURCE) -C $(@D) --strip-components=1 +endef + +define AETHERSX2_INSTALL_TARGET_CMDS + mkdir -p $(TARGET_DIR)/usr/aethersx2/libs + cp -pr $(@D)/usr/share/* $(TARGET_DIR)/usr/aethersx2/ + chmod 755 $(TARGET_DIR)/usr/aethersx2/aethersx2 + # Mesa/GLVND shim libs (libGLX/libOpenGL/libGLdispatch/libGLX_mesa) + # plus an older-ABI libpcap that the binary link-time-depends on but + # never actually calls at runtime here - Renderer is forced to + # Vulkan=14 in the configgen generator, so real GLX is never invoked; + # these only satisfy the dynamic linker's NEEDED-library pass. Pulled + # from a real, working Debian aarch64 install and live-tested on this + # exact board first rather than built from source or fetched fresh at + # build time, to guarantee they match what was actually verified + # working (see project_odroid_retro.md 2026-08-16 AetherSX2 section). + cp -p $(AETHERSX2_PKGDIR)/libs/*.so.* $(TARGET_DIR)/usr/aethersx2/libs/ +endef + +$(eval $(generic-package)) +$(eval $(emulator-info-package)) diff --git a/package/batocera/emulators/aethersx2/libs/libGL.so.1 b/package/batocera/emulators/aethersx2/libs/libGL.so.1 new file mode 100644 index 0000000000000000000000000000000000000000..f1d9777d570f03eb1d63c2f307af91353d58a470 GIT binary patch literal 936040 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%0DZn+Ac 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