diff --git a/.gitignore b/.gitignore index a81283971ce..52039134114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /output +/output-* /dl /configs/batocera-*_defconfig /configs/batocera-board.local.common diff --git a/.gitmodules b/.gitmodules index ea4c1d504ce..bdb839c0d7b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "buildroot"] path = buildroot - url = https://github.com/batocera-linux/buildroot.git + url = https://github.com/Pyohwan/buildroot.git diff --git a/Config.in b/Config.in index 310a17e8830..3804e514f20 100644 --- a/Config.in +++ b/Config.in @@ -56,6 +56,12 @@ menu "System" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-rk3326/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/rocknix-abl/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-odroid-m2/Config.in" + # Pre-dates upstream's "rework uboot for rk356x devices" (01273a1f5f), + # which deleted uboot-odroid-m1 in favor of the unified uboot-rk356x + # below. Our board still uses this standalone package (known-working, + # matches this board's actual VU8M/SPI boot flow) - migrating to + # uboot-rk356x is a separate, deliberate follow-up, not a merge fix. + source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-odroid-m1/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-rk356x/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-rk3576/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/boot/uboot-gameforce-ace/Config.in" @@ -92,6 +98,7 @@ menu "System" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mesa-panfork/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-G610/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-G52/Config.in" + source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-g52-odroidm1/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-G31/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-g31-gbm/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/gpu/mali-mp400-wayland/Config.in" @@ -110,6 +117,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 @@ -253,6 +261,7 @@ menu "Emulators" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-gp32emu/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-fbalpha/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-fbneo/Config.in" + source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-fbneo-korean/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-gw/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-prboom/Config.in" source "$BR2_EXTERNAL_BATOCERA_PATH/package/batocera/emulators/retroarch/libretro/libretro-mrboom/Config.in" @@ -370,6 +379,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/README-odroidm1.en.md b/README-odroidm1.en.md new file mode 100644 index 00000000000..0548b3b64f8 --- /dev/null +++ b/README-odroidm1.en.md @@ -0,0 +1,97 @@ +# ODROID-M1 (bsp-odroidm1) + +*English | [한국어](README-odroidm1.ko.md)* + +## Features added on top of vanilla Batocera + +- Uses Hardkernel's own BSP kernel (**Linux 6.1.141**) instead of mainline + - Required for VU8M, mono audio, and USB3.0 support +- VU8M (Hardkernel's 8" DSI touchscreen) support +- Uses the vendor Mali G52 blob (g29p1 generation) - GLES + Vulkan +- Wayland (labwc) compositor support - enables standalone Qt-based emulators (PCSX2/Dolphin/AetherSX2, etc.) to actually run +- Fixed mono speaker volume/output routing (default volume was far too quiet) +- FBNeo Korean-patched core (`fbneo_korean`) set as default - includes DsNo's patch, which fixes a CRC32 mismatch that caused Korean-patched ROMs to be rejected +- AetherSX2 (PS2) set as default - an alternative core that avoids a Mali GPU fault that mainline PCSX2-family cores hit +- Vulkan rendering support for PS2/GameCube/Dreamcast/PSP and other major systems +- Re-tuned per-system "auto" core/API defaults to match actual measured best performance + +## Setup + +### Display (HDMI / VU8M) + +- **HDMI**: enabled by default, no configuration needed (tested up to 1080p). +- **VU8M**: disabled by default. Enable it in `config.ini` on the **BATOCERA** partition (the drive that shows up when you plug the SD card into a PC): + ``` + overlays="display_vu8m" + ``` + Set it back to `overlays=""` to disable. The DSI panel has no hotplug detection, so once the overlay is enabled it's always reported as "connected" whether the panel is physically attached or not - turn it off when not in use. + +### Backglass / screen rotation + +Everything below assumes VU8M is already enabled via `overlays="display_vu8m"` in `config.ini` above. + +**Note**: the settings below are stored in `system/batocera.conf`, which only exists after the board has **booted at least once** and initialized the SHARE partition - a freshly flashed SD card has neither this file nor the `system` folder at all (Batocera creates the whole default directory structure - `roms`/`bios`/`saves`/`system`, etc. - on first boot). Do this after the first boot. + +**Using it as a backglass**: to use VU8M as a secondary screen showing box art/game info while playing on the main screen (HDMI), go to **MAIN MENU → SYSTEM SETTINGS → HARDWARE → MULTISCREENS → BACKGLASS / INFORMATION SCREEN** and set VIDEO OUTPUT to VU8M (`DSI-1`). **This must be set** for ES to actually configure that output (resolution/rotation) - leaving it blank just gets you an unused extended screen tacked onto HDMI with nothing rendered on it. + +**Exception - NDS dual-screen**: NDS naturally has two screens (top/bottom). Enabling both HDMI and VU8M together with the `melonDS` core lets you output NDS's top and bottom screens to the two physical displays separately. However, melonDS performs poorly on this board (much slower than the default `drastic` core even with GPU acceleration on), making it **impractical for regular use** - stick with the default `drastic` (single screen) unless you specifically need the dual-screen setup. + +**Rotation**: best done from the ES menu - note that both options are labeled "SCREEN ROTATION", so you have to go by location: +- **MAIN MENU → SYSTEM SETTINGS → HARDWARE → SCREEN ROTATION**: rotates the main screen (HDMI). +- **MAIN MENU → SYSTEM SETTINGS → HARDWARE → MULTISCREENS → BACKGLASS / INFORMATION SCREEN → SCREEN ROTATION**: rotates VU8M (the backglass). (Saved together with the VIDEO OUTPUT setting above.) + +These values are stored in `system/batocera.conf` on the **SHARE partition** (the second drive that shows up when you plug the SD card into a PC). Mount that partition and edit the file directly, or over SSH: +``` +batocera-settings-set global.videooutput2 DSI-1 # assign VU8M as the 2nd screen (backglass) +batocera-settings-set display.rotate2.DSI-1 3 # rotation value for that screen +``` +A **reboot** is needed for this to take effect (EmulationStation only sets up outputs/rotation at startup): +``` +reboot +``` +Main-screen rotation uses `display.rotate.=` per connector (e.g. `display.rotate.HDMI-A-1`), falling back to the global `display.rotate=` if unset. `` is `0` (normal), `1` (90°), `2` (180°), `3` (270°) - main-screen rotation is also automatically picked up by the boot splash on the next reboot (vanilla mechanism, no extra setup needed). `display.rotate2` (backglass) doesn't apply here - the splash never draws anything to the 2nd screen. + +## Per-system performance results and recommended core/API + +Measured on **`batocera44-odroidm1-v1.0.0`** (this doc always tracks the latest version - re-verify if the relevant core/kernel/blob changes in a later release), Power Mode fixed to High Performance, Avg FPS / 1% low (frametime-based, an industry-standard metric). **The core/API actually selected by "auto" is bolded.** + +| System | Game | Core | API | Avg FPS | 1% Low | +| --- | --- | --- | --- | --- | --- | +| Genesis/Mega Drive | Altered Beast | **genesisplusgx** | — | **60.0** | 50.3 | +| | | genesisplusgx-expanded | — | 60.0 | 47.6 | +| | | picodrive | — | 60.0 | 48.5 | +| FBNeo | Ninja Baseball Bat Man | **fbneo_korean** | — | **60.0** | 41.0 | +| Saturn | Strikers 1945 | **yabasanshiro** | GLES | **59.3** | 24.3 | +| | | beetle-saturn | GLES | 22.5 | 16.8 | +| | | beetle-saturn | Vulkan | 22.8 | 16.4 | +| PSX | Soul Blade | **pcsx_rearmed** | GLES | **60.0** | 38.9 | +| | | pcsx_rearmed | Vulkan | 60.0 | 42.0 | +| | | swanstation | GLES | 58.5 | 32.1 | +| | | swanstation | Vulkan | 59.3 | 34.5 | +| | | mednafen_psx | GLES | 29.0 | 19.6 | +| | | mednafen_psx | Vulkan | 31.3 | 20.7 | +| N64 | Yoshi's Story | standalone mupen64plus(**rice**) | GLES | **59.6** | 30.3 | +| | | standalone mupen64plus(glide64mk2) | GLES | 54.9 | 13.0 | +| | | standalone mupen64plus(gliden64) | GLES | 10.3 | 3.6 | +| | | libretro mupen64plus-next | GLES | 59.5 | 24.5 | +| | | libretro parallel_n64 | GLES | 56.2 | 17.9 | +| Dreamcast | Soulcalibur | **standalone flycast** | **Vulkan** | **55.5** | 11.7 | +| | | standalone flycast | GLES | 56.4 | 13.0 | +| | | libretro flycast | GLES | 55.9 | 10.1 | +| | | libretro flycast | Vulkan | 56.0 | 13.1 | +| PS2 | 2002 FIFA World Cup (Korea) | **AetherSX2** | **Vulkan** | **23.8** | 4.2 | +| GameCube | Pikmin | **standalone Dolphin** | **Vulkan** | **29.7** | 8.4 | +| PSP | Tekken 6 | **standalone PPSSPP** | **Vulkan** | **58.9** | 27.0 | +| | | standalone PPSSPP | GLES | 54.0 | 17.7 | +| | | libretro ppsspp | GLES | 54.2 | 16.1 | +| | | libretro ppsspp | Vulkan | 54.1 | 27.0 | +| NDS | (excluded from the matrix - all 3 tracks cleared 60fps with 99%+ headroom, so a full sweep wasn't needed) | **drastic** | — | — | — | + +**Notes**: +- Genesis/FBNeo use software 2D rendering, so the GLES/Vulkan API distinction doesn't apply. +- The libretro Vulkan cores for N64/Dreamcast (mupen64plus-next, parallel_n64) have a known issue where audio plays but no video appears, so they're excluded above (GLES-only comparison). +- Wii isn't benchmarked separately but is expected to behave the same as GameCube (standalone Dolphin + Vulkan), so the same recommendation applies. + +## Known limitations + +- **PS2 `pcsx2` core** (mainline PCSX2 family): the newer GS command patterns these cores use trigger a genuine Mali G52 GPU hardware fault (`DATA_INVALID_FAULT`) - confirmed via dmesg logs and actual game hangs. AetherSX2 (a PCSX2-family fork using older GS command patterns) avoids this fault and is used as the default. `pcsx2` is left installed and selectable in EmulationStation for anyone who wants to experiment with it. diff --git a/README-odroidm1.ko.md b/README-odroidm1.ko.md new file mode 100644 index 00000000000..442edd8bb70 --- /dev/null +++ b/README-odroidm1.ko.md @@ -0,0 +1,97 @@ +# ODROID-M1 (bsp-odroidm1) + +*[English](README-odroidm1.en.md) | 한국어* + +## Vanilla Batocera 대비 추가된 기능 + +- 하드커널 자체 BSP 커널(**Linux 6.1.141**)을 씀 — 메인라인 대신 사용 + - VU8M, 모노 오디오, USB3.0 지원에 필요 +- VU8M(하드커널 8인치 DSI 터치스크린) 지원 +- 벤더 Mali G52 blob(g29p1 세대)을 씀 — GLES + Vulkan +- Wayland(labwc) 컴포지터 지원 — PCSX2/Dolphin/AetherSX2 등 standalone Qt 기반 에뮬레이터 실사용 가능 +- 모노 스피커 볼륨/출력 라우팅 수정(기본 볼륨이 너무 작던 문제 해결) +- FBNeo 한국어 패치 코어(`fbneo_korean`)를 기본값으로 씀 — 한국어 패치 롬의 CRC32 불일치 현상을 해결한 DsNo님의 패치를 넣은 것 +- AetherSX2(PS2)를 기본값으로 넣음 — mainline PCSX2 계열이 겪는 Mali GPU 폴트를 피하는 대안 코어 +- PS2/GameCube/Dreamcast/PSP 등 주요 시스템 Vulkan 렌더링 지원 +- 시스템별 자동(auto) 코어/API 선택을 실측 최고 성능 기준으로 다시 맞춤 + +## 설정 + +### 디스플레이 (HDMI / VU8M) + +- **HDMI**: 기본 활성화, 별도 설정 불필요(1080p까지 테스트 완료). +- **VU8M**: 기본 비활성화. **BATOCERA** 파티션(SD카드를 컴퓨터에 꽂으면 보이는 드라이브)의 `config.ini`에서 켤 것: + ``` + overlays="display_vu8m" + ``` + 끄려면 다시 `overlays=""`. DSI 패널은 핫플러그 감지가 안 되므로, 켜두면 실제로 패널이 꽂혀있지 않아도 항상 "연결됨"으로 잡힘 — 안 쓸 땐 꺼둘 것. + +### 백글래스 / 화면 회전 + +아래 내용은 모두 위 `config.ini`에서 `overlays="display_vu8m"`로 VU8M을 켠 상태를 전제로 함. + +**주의**: 아래 설정이 저장되는 `system/batocera.conf`는 기기가 **최초 1회 부팅**해서 SHARE 파티션을 초기화해야 생김 — 갓 구운 SD카드를 PC에 바로 꽂으면 이 파일도, `system` 폴더 자체도 없음(최초 부팅 시 Batocera가 SHARE 파티션에 `roms`/`bios`/`saves`/`system` 등 전체 기본 디렉토리 구조를 만듦). 기기를 한 번 부팅시킨 뒤에 설정할 것. + +**백글래스로 쓰기**: VU8M을 메인 화면(HDMI) 게임 중 박스아트/게임 정보를 보여주는 보조 화면으로 쓰려면, ES 메뉴 **MAIN MENU → SYSTEM SETTINGS → HARDWARE → MULTISCREENS → BACKGLASS / INFORMATION SCREEN**에서 VIDEO OUTPUT을 VU8M(`DSI-1`)으로 지정. **이 지정을 해야만** ES가 그 출력을 실제로 구성(해상도/회전 적용)함 — 비워두면 HDMI 옆에 이어붙는 미사용 확장 화면으로만 잡히고 아무것도 제대로 안 나옴. + +**예외 — NDS 듀얼스크린**: NDS는 원래 화면이 위/아래 두 개라, HDMI+VU8M을 동시에 켜고 `melonDS` 코어를 쓰면 실제 물리 화면 두 개에 NDS의 위/아래 화면을 각각 나눠 출력할 수 있음. 다만 melonDS는 이 보드에서 성능이 안 좋아서(GPU가속 켜도 기본 권장 코어 drastic 대비 훨씬 느림) **사실상 실사용은 힘듦** — 듀얼스크린이 정말 꼭 필요한 경우가 아니면 기본값인 drastic(단일 화면)을 쓸 것. + +**회전 설정**: ES 메뉴에서 하는 게 정석 — 단, 라벨이 둘 다 "SCREEN ROTATION"이라 위치로 구분해야 함: +- **MAIN MENU → SYSTEM SETTINGS → HARDWARE → SCREEN ROTATION**: 메인 화면(HDMI) 회전. +- **MAIN MENU → SYSTEM SETTINGS → HARDWARE → MULTISCREENS → BACKGLASS / INFORMATION SCREEN → SCREEN ROTATION**: VU8M(백글래스) 회전. (여기서 고른 값이 위 VIDEO OUTPUT과 함께 저장됨) + +이 값들은 **SHARE 파티션**(SD카드를 컴퓨터에 꽂으면 보이는 두 번째 드라이브)의 `system/batocera.conf`에 저장됨. 이 파티션을 마운트해서 직접 편집하거나, SSH로: +``` +batocera-settings-set global.videooutput2 DSI-1 # VU8M을 2번째 화면(백글래스)으로 지정 +batocera-settings-set display.rotate2.DSI-1 3 # 그 화면의 회전값 +``` +설정 후 **재부팅**해야 적용됨(EmulationStation이 시작 시점에만 출력/회전을 구성함): +``` +reboot +``` +메인 화면 회전은 커넥터별로 `display.rotate.<커넥터이름>=`(예: `display.rotate.HDMI-A-1`), 없으면 전역 `display.rotate=`. ``은 `0`(정상), `1`(90도), `2`(180도), `3`(270도) — 메인 화면 회전은 부팅 스플래시에도 재부팅 시점에 자동 반영됨(vanilla 메커니즘, 별도 설정 불필요). `display.rotate2`(백글래스)는 여기 해당 안 됨 — 스플래시는 2번째 화면엔 아무것도 안 그림. + +## 시스템별 성능 측정 결과 및 권장 코어/API + +측정 기준 버전: **`batocera44-odroidm1-v1.0.0`**(이 문서는 릴리스마다 최신 상태로 유지 — 이후 버전에서 관련 코어/커널/blob이 바뀌면 재검증 필요), Power Mode = High Performance 고정, Avg FPS/1% Low 기준(프레임타임 기반, 업계 표준 지표). **auto로 실제 적용되는 코어/API는 굵게 표시.** + +| 시스템 | 게임 | 코어 | API | Avg FPS | 1% Low | +| --- | --- | --- | --- | --- | --- | +| Genesis/Mega Drive | Altered Beast | **genesisplusgx** | — | **60.0** | 50.3 | +| | | genesisplusgx-expanded | — | 60.0 | 47.6 | +| | | picodrive | — | 60.0 | 48.5 | +| FBNeo | 야구격투 리그맨 | **fbneo_korean** | — | **60.0** | 41.0 | +| Saturn | Strikers 1945 | **yabasanshiro** | GLES | **59.3** | 24.3 | +| | | beetle-saturn | GLES | 22.5 | 16.8 | +| | | beetle-saturn | Vulkan | 22.8 | 16.4 | +| PSX | Soul Blade | **pcsx_rearmed** | GLES | **60.0** | 38.9 | +| | | pcsx_rearmed | Vulkan | 60.0 | 42.0 | +| | | swanstation | GLES | 58.5 | 32.1 | +| | | swanstation | Vulkan | 59.3 | 34.5 | +| | | mednafen_psx | GLES | 29.0 | 19.6 | +| | | mednafen_psx | Vulkan | 31.3 | 20.7 | +| N64 | 요시스토리 | standalone mupen64plus(**rice**) | GLES | **59.6** | 30.3 | +| | | standalone mupen64plus(glide64mk2) | GLES | 54.9 | 13.0 | +| | | standalone mupen64plus(gliden64) | GLES | 10.3 | 3.6 | +| | | libretro mupen64plus-next | GLES | 59.5 | 24.5 | +| | | libretro parallel_n64 | GLES | 56.2 | 17.9 | +| Dreamcast | Soulcalibur | **standalone flycast** | **Vulkan** | **55.5** | 11.7 | +| | | standalone flycast | GLES | 56.4 | 13.0 | +| | | libretro flycast | GLES | 55.9 | 10.1 | +| | | libretro flycast | Vulkan | 56.0 | 13.1 | +| PS2 | 2002 FIFA World Cup(Korea) | **AetherSX2** | **Vulkan** | **23.8** | 4.2 | +| GameCube | Pikmin | **standalone Dolphin** | **Vulkan** | **29.7** | 8.4 | +| PSP | Tekken 6 | **standalone PPSSPP** | **Vulkan** | **58.9** | 27.0 | +| | | standalone PPSSPP | GLES | 54.0 | 17.7 | +| | | libretro ppsspp | GLES | 54.2 | 16.1 | +| | | libretro ppsspp | Vulkan | 54.1 | 27.0 | +| NDS | (매트릭스 측정 제외 — 3트랙 모두 99%+ 여유로 일찌감치 결론남) | **drastic** | — | — | — | + +**참고**: +- Genesis/FBNeo는 소프트웨어 2D 렌더링이라 API(GLES/Vulkan) 구분 자체가 의미 없음. +- N64/Dreamcast의 libretro Vulkan 코어(mupen64plus-next, parallel_n64)는 소리만 나오고 화면이 안 뜨는 알려진 문제가 있어 위 표에서 제외함(GLES만 비교 가능). +- Wii는 GameCube와 같은 원리(standalone Dolphin + Vulkan)로 별도 성능 측정은 안 했지만 동일하게 권장. + +## 알려진 제약 + +- **PS2 `pcsx2` 코어**(mainline PCSX2 계열): 이 코어들이 쓰는 신형 GS 커맨드 패턴이 Mali G52 GPU의 진짜 하드웨어 폴트(`DATA_INVALID_FAULT`)를 유발함 — dmesg 로그와 실제 게임 멈춤으로 확인됨. AetherSX2(구형 GS 커맨드 패턴을 쓰는 PCSX2 계열 포크)는 이 폴트를 회피해서 기본값으로 씀. `pcsx2`도 EmulationStation에 설치+선택 가능한 상태로 남겨뒀음(실험해보고 싶은 사람용). diff --git a/board/batocera/fsoverlay/etc/init.d/S02resize b/board/batocera/fsoverlay/etc/init.d/S02resize index 15321f83adb..e17b74fb025 100755 --- a/board/batocera/fsoverlay/etc/init.d/S02resize +++ b/board/batocera/fsoverlay/etc/init.d/S02resize @@ -7,7 +7,7 @@ LOG="/tmp/resize.log" test "$1" != "start" && exit 0 # Get the active console. If it fails, default to tty1 as a fallback. -ACTIVE_TTY_PATH="/dev/tty$(fgconsole 2>/dev/null || echo 1)" +ACTIVE_TTY_PATH="/dev/tty$(fgconsole 2>/dev/null || echo /dev/tty1)" echo "Detected active console: ${ACTIVE_TTY_PATH}" >> "$LOG" # true if triggers are not available or not set to do so @@ -15,33 +15,6 @@ if ! grep -qE '^[ ]*autoresize[ ]*=[ ]*true[ ]*$' "${BOOTCONF}" && ! grep -qE '^ exit 0 fi -# We're actually going to draw a dialog, so it needs somewhere real to land. -# This runs early enough in boot (S02) that the console is still on the -# "(S) dummy device" vtcon and nothing written to it reaches the physical -# display yet - the DRM driver hasn't bound and switched fbcon over to a -# real framebuffer (confirmed via dmesg on the ODROID-M1: "rockchip-drm -# ... bound" and "Console: switching to colour frame buffer device" land -# around 16s into boot, well after this script would otherwise already be -# drawing). Wait for a vtconsole to actually report itself as a bound frame -# buffer device before drawing anything; give up after a timeout so boards -# without a DRM fbcon (or where it genuinely never binds) don't hang here. -wait_for_fbcon() { - local waited=0 - while [ "${waited}" -lt 100 ]; do - for vtcon in /sys/class/vtconsole/vtcon*; do - if [ -f "${vtcon}/bind" ] && [ "$(cat "${vtcon}/bind" 2>/dev/null)" = "1" ] \ - && grep -qi "frame buffer" "${vtcon}/name" 2>/dev/null; then - echo "fbcon ready after ${waited}0ms (${vtcon})" >> "$LOG" - return 0 - fi - done - sleep 0.1 - waited=$((waited + 1)) - done - echo "fbcon not ready after ${waited}0ms, proceeding anyway" >> "$LOG" -} -wait_for_fbcon - # Remove the trigger(s) remove_trigger() { local trigger="$1" 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); diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S02resize b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S02resize new file mode 100755 index 00000000000..15321f83adb --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S02resize @@ -0,0 +1,275 @@ +#!/bin/bash + +BOOTCONF="/boot/batocera-boot.conf" +LOG="/tmp/resize.log" + +# only at start +test "$1" != "start" && exit 0 + +# Get the active console. If it fails, default to tty1 as a fallback. +ACTIVE_TTY_PATH="/dev/tty$(fgconsole 2>/dev/null || echo 1)" +echo "Detected active console: ${ACTIVE_TTY_PATH}" >> "$LOG" + +# true if triggers are not available or not set to do so +if ! grep -qE '^[ ]*autoresize[ ]*=[ ]*true[ ]*$' "${BOOTCONF}" && ! grep -qE '^[ ]*format-internal[ ]*=' "${BOOTCONF}"; then + exit 0 +fi + +# We're actually going to draw a dialog, so it needs somewhere real to land. +# This runs early enough in boot (S02) that the console is still on the +# "(S) dummy device" vtcon and nothing written to it reaches the physical +# display yet - the DRM driver hasn't bound and switched fbcon over to a +# real framebuffer (confirmed via dmesg on the ODROID-M1: "rockchip-drm +# ... bound" and "Console: switching to colour frame buffer device" land +# around 16s into boot, well after this script would otherwise already be +# drawing). Wait for a vtconsole to actually report itself as a bound frame +# buffer device before drawing anything; give up after a timeout so boards +# without a DRM fbcon (or where it genuinely never binds) don't hang here. +wait_for_fbcon() { + local waited=0 + while [ "${waited}" -lt 100 ]; do + for vtcon in /sys/class/vtconsole/vtcon*; do + if [ -f "${vtcon}/bind" ] && [ "$(cat "${vtcon}/bind" 2>/dev/null)" = "1" ] \ + && grep -qi "frame buffer" "${vtcon}/name" 2>/dev/null; then + echo "fbcon ready after ${waited}0ms (${vtcon})" >> "$LOG" + return 0 + fi + done + sleep 0.1 + waited=$((waited + 1)) + done + echo "fbcon not ready after ${waited}0ms, proceeding anyway" >> "$LOG" +} +wait_for_fbcon + +# Remove the trigger(s) +remove_trigger() { + local trigger="$1" + mount -o remount,rw /boot + sed -i -e "s/^[ ]*${trigger}/#${trigger}/g" "${BOOTCONF}" + mount -o remount,ro /boot +} + +# UI Output with dialog, default colorset +function dialogoutput() { + local percent="$1" + local text="Do not switch off your device!" + + # Get terminal dimensions, with fallbacks for safety + local term_height=$(tput lines 2>/dev/null) + local term_width=$(tput cols 2>/dev/null) + if ! [[ "$term_height" =~ ^[0-9]+$ ]] || ! [[ "$term_width" =~ ^[0-9]+$ ]]; then + term_height=24 + term_width=80 + fi + + # Calculate dialog width (90% of screen, min 45 cols) + local dialog_width=$((term_width * 9 / 10)) + if (( dialog_width < 45 )); then dialog_width=45; fi + + # Calculate dialog height based on number of items + padding + local num_items=$((${#arr[@]} / 2)) + local dialog_height=$((num_items + 8)) + + # Final check to ensure dialog fits on screen + if (( dialog_height >= term_height )); then dialog_height=$((term_height - 1)); fi + if (( dialog_width >= term_width )); then dialog_width=$((term_width - 1)); fi + + dialog --backtitle "batocera.linux" --title " Resizing & Formatting Partition " \ + --mixedgauge "$text" "$dialog_height" "$dialog_width" "$percent" "${arr[@]}" &>"${ACTIVE_TTY_PATH}" +} + +# Display error with timeout +function display_error() { + # Get terminal dimensions, with fallbacks for safety + local term_height=$(tput lines 2>/dev/null) + local term_width=$(tput cols 2>/dev/null) + if ! [[ "$term_height" =~ ^[0-9]+$ ]] || ! [[ "$term_width" =~ ^[0-9]+$ ]]; then + term_height=24 + term_width=80 + fi + + # Calculate dialog width (80% of screen, min 50 cols) + local dialog_width=$((term_width * 8 / 10)) + if (( dialog_width < 50 )); then dialog_width=50; fi + local dialog_height=8 + + # Final check to ensure dialog fits on screen + if (( dialog_height >= term_height )); then dialog_height=$((term_height - 1)); fi + if (( dialog_width >= term_width )); then dialog_width=$((term_width - 1)); fi + + dialog --title "Resize Error" --timeout 10 \ + --msgbox "\nPlease check the resize log at: /tmp/resize.log" \ + "$dialog_height" "$dialog_width" &>"${ACTIVE_TTY_PATH}" + exit 1 +} + +# Partition size calculation +get_partition_size_mb() { + local partition="$1" + local size_kb=$(blockdev --getsize64 "$partition") + echo $((size_kb / 1024 / 1024)) +} + +# Executing parameters and watch background pid +# Changes text messages parsed to dialog --mixedgauge +function textoutput() +{ + local cmd_str="$3" + local percent="$2" + local pid ret + echo "cmd: ${cmd_str}" >> "$LOG" + + # Handle empty commands (for skipped steps) + if [ -z "$cmd_str" ]; then + arr[$1]=0 # msg: Set to success + dialogoutput "$percent" + return 0 + fi + + eval "$cmd_str" >> "$LOG" 2>&1 & + pid=$! + arr[$1]=7 #msg: In Progress + dialogoutput "$percent" + wait $pid + ret=$? + echo "cmd code:$ret" >> "$LOG" + arr[$1]=$ret #msg: Depends from return value + if [ $ret -ne 0 ]; then + display_error + fi + return $ret +} + +# only when resizing is wanted +if grep -qE '^[ ]*autoresize[ ]*=[ ]*true[ ]*$' "${BOOTCONF}"; then + # Preparing text arrays + arr=( + "Aligning GPT table" "Pending" + "Resizing partition" "Pending" + "Notifying kernel" "Pending" + "Formatting /userdata" "Pending" + "Checking filesystem" "Pending" + "Syncing data to disk" "Pending" + "Removing trigger" "Pending" + ) + + # --- BEGIN RESIZE --- + + # Get /userdata partition details + PART=$(batocera-part "share_internal") + PARTNUM=$(batocera-part "share_internal_num") + echo "Partition name: $PART & number: $PARTNUM" >> "$LOG" + + # Check the initial partition size + PARTSIZE=$(get_partition_size_mb "$PART") + echo "Initial partition size: ${PARTSIZE} MB" >> "$LOG" + + # Determine if we should format based on initial size + FORMAT_ALLOWED=false + if [ "$PARTSIZE" -eq 256 ] || [ "$PARTSIZE" -eq 512 ]; then + FORMAT_ALLOWED=true + echo "Initial partition size is ${PARTSIZE}MB, formatting will be allowed" >> "$LOG" + else + echo "Initial partition size is ${PARTSIZE}MB, formatting will be skipped" >> "$LOG" + fi + + # boot disk + DISK=$(batocera-part prefix "${PART}") + echo "Disk = $DISK" >> "$LOG" + + TABLETYPE=$(parted -s ${DISK} print | grep 'Partition Table' | awk '{print $3}') + echo "Disk partition table type = $TABLETYPE" >> "$LOG" + + # only for ext4 + PARTTYPE=$(blkid "${PART}" | sed -e s+'^.* TYPE="\([^"]*\)\".*'+'\1'+) + test "${PARTTYPE}" != "ext4" && exit 0 + echo "Partition type = ${PARTTYPE}" >> "$LOG" + + # textoutput "Message" "percentage" "command call" + for i in 1 3 5 7 9 11 13; do + case $i in + 1) + # check for a GPT partition + if [ "${TABLETYPE}" = "gpt" ]; then + # move backup GPT data structures to the end of the disk + echo "Step $i: Moving 2nd GPT table to the end of the disk" >> "$LOG" + textoutput $i 10 "sgdisk -e ${DISK}" + else + echo "Step $i: Skipping GPT table move, disk is not GPT" >> "$LOG" + textoutput $i 10 "" + fi + ;; + 3) + # resize the partition + echo "Step $i: Resizing the partition to 100%" >> "$LOG" + textoutput $i 30 "parted -s -m -f ${DISK} resizepart ${PARTNUM} 100%" + ;; + 5) + # update the kernel + echo "Step $i: Updating the kernel" >> "$LOG" + textoutput $i 40 "partprobe ${DISK}; sleep 2" + ;; + 7) + if test "${PARTTYPE}" = "ext4" && "$FORMAT_ALLOWED" = true + then + echo "Step $i: Formatting the ext4 file system" >> "$LOG" + textoutput $i 60 "mkfs.ext4 -L SHARE -q -F -F ${PART}" + else + echo "Step $i: Skipping formatting due to partition size or type" >> "$LOG" + textoutput $i 60 "" + fi + ;; + 9) + # check & resize the ext4 file system + if test "${PARTTYPE}" = "ext4" + then + echo "Step $i: Checking ext4 file system" >> "$LOG" + textoutput $i 70 "e2fsck -f -p ${PART}" + fi + ;; + 11) + # finally disk sync + echo "Step $i: Final sync" >> "$LOG" + textoutput $i 80 "sync" + ;; + 13) + # remove the trigger + textoutput $i 90 "remove_trigger autoresize" + ;; + esac + done + +else + ###### format internal share ##### + FORMAT_INTERNAL_TYPE=$(grep -E '^[ ]*format-internal[ ]*=.*$' "${BOOTCONF}" | head -1 | sed -e s+"^[ ]*format-internal[ ]*=[ ]*\(.*\)[ ]*$"+"\1"+) + if test -n "${FORMAT_INTERNAL_TYPE}"; then + # Preparing text arrays + arr=( + "Formatting /userdata" "Pending" + "Removing trigger" "Pending" + ) + PART=$(batocera-part "share_internal") + + case "${FORMAT_INTERNAL_TYPE}" in + "btrfs") + textoutput 1 50 "mkfs.btrfs -L SHARE -f ${PART}" + ;; + "ext4") + textoutput 1 50 "mkfs.ext4 -L SHARE -q -F -F ${PART}" + ;; + "exfat") + textoutput 1 50 "mkfs.exfat -n SHARE ${PART}" + ;; + *) + # do nothing + esac + # remove the trigger + textoutput 3 90 "remove_trigger format-internal" + fi +fi + +#Cleanup, restore screen, set progress of last item to 100% +dialogoutput 100 +clear > "${ACTIVE_TTY_PATH}" +exit 0 diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S03system-splash b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S03system-splash new file mode 100755 index 00000000000..e74eb16e2c0 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S03system-splash @@ -0,0 +1,156 @@ +#!/bin/sh + +# This is the BATOCERA system splash service +# It shows the unique BATOCERA logo in early boot stage + +LOG_FILE="/var/log/splash.log" + +do_start () { + LOG_DIR="${LOG_FILE%/*}" + [ -d "${LOG_DIR}" ] || mkdir -p "${LOG_DIR}" + exec >> "${LOG_FILE}" 2>&1 + + if [ ! -e /dev/fb0 ]; then + i=0 + while [ ! -e /dev/fb0 ] && [ $i -lt 50 ]; do + sleep 0.1 + i=$((i + 1)) + done + if [ ! -e /dev/fb0 ]; then + echo "Error: Framebuffer device /dev/fb0 not found." + exit 1 + fi + fi + + # Default image + image="/usr/share/batocera/splash/boot-logo.png" + # Standard FBV options + FBV_OPTS="-f -e -i -y" + # Variables for screen dimensions + WIDTH="" + HEIGHT="" + + # Resolve video rotation first so we know how to crop the image + V_BOARD_MODEL=$(batocera-model) + video_rotation="" + BOOT_CONF="/boot/batocera-boot.conf" + + # batocera-boot.conf already gets display.rotate/display.rotate. + # synced into it from batocera.conf by S65values4boot on every + # shutdown/reboot (vanilla mechanism) - no need to duplicate the setting + # in config.ini as well. + if [ -f "${BOOT_CONF}" ]; then + specific_key=$(awk -F'=' '/^[[:space:]]*display\.rotate\./{print $1; exit}' "${BOOT_CONF}" | tr -d '[:space:]') + if [ -n "${specific_key}" ]; then + video_rotation=$(/usr/bin/batocera-settings-get -f "${BOOT_CONF}" "${specific_key}") + fi + [ -z "${video_rotation}" ] && \ + video_rotation=$(/usr/bin/batocera-settings-get -f "${BOOT_CONF}" "display.rotate") + fi + + if [ -z "${video_rotation}" ]; then + MODEL_CONF="/usr/share/batocera/sysconfigs/batocera.conf.${V_BOARD_MODEL}" + if [ -f "${MODEL_CONF}" ]; then + # Match the *currently active* connector's rotate key specifically + # (same detection S65values4boot already uses) - a board can have + # more than one display.rotate. line for different + # outputs (e.g. a small secondary panel that needs rotating while + # the primary doesn't), and grabbing "whichever one happens to be + # first in the file" applied a completely unrelated connector's + # rotation to the splash (confirmed live on the ODROID-M1: with + # only display.rotate.DSI-1 defined, an HDMI-only boot picked + # that 90-degree value up and rendered the boot logo sideways). + effectiveDrmOutput=$(for GPU in /dev/dri/card*; do batocera-drminfo "${GPU}" "current" 2>/dev/null | grep -oP '^[0-9]+\.[0-9]+:\K[^ ]+'; done | head -1) + rotation_line="" + if [ -n "${effectiveDrmOutput}" ]; then + rotation_line=$(grep -E "^display\.rotate\.${effectiveDrmOutput}=" "${MODEL_CONF}" | head -1) + fi + if [ -z "${rotation_line}" ]; then + # No connector-specific match (or none detected yet this + # early in boot) - fall back to a bare "display.rotate=" + # default if the board sysconfig defines one. + rotation_line=$(grep -E '^display\.rotate=' "${MODEL_CONF}" | head -1) + fi + if [ -n "${rotation_line}" ]; then + video_rotation=$(echo "${rotation_line}" | cut -d'=' -f2) + fi + fi + fi + + video_rotation=$(echo "${video_rotation}" | tr -d '[:space:]') + echo "Final Rotation Value: [${video_rotation}]" + + # Get the screen's physical dimensions + if [ -r /sys/class/graphics/fb0/modes ]; then + if read -r MODE_STR < /sys/class/graphics/fb0/modes; then + DIM_PART=${MODE_STR#*:} + WIDTH=${DIM_PART%%x*} + remaining=${DIM_PART#*x} + HEIGHT=${remaining%%[!0-9]*} + fi + + if [ -n "${WIDTH}" ] && [ -n "${HEIGHT}" ]; then + echo "Resolution detected via modes: ${WIDTH}x${HEIGHT}" + + # Determine target size for cropping. + # If rotated 90 or 270 degrees (1 or 3), swap width and height. + if [ "${video_rotation}" = "1" ] || [ "${video_rotation}" = "3" ]; then + CROP_WIDTH="${HEIGHT}" + CROP_HEIGHT="${WIDTH}" + else + CROP_WIDTH="${WIDTH}" + CROP_HEIGHT="${HEIGHT}" + fi + + # Automatically crops and centers the master logo to target aspect ratio + cropped_image="/tmp/boot-logo-cropped.bmp" + if [ -f "${image}" ]; then + echo "Pre-cropping logo to logical size ${CROP_WIDTH}x${CROP_HEIGHT} via FFmpeg..." + if ffmpeg -y -threads 1 -sws_flags fast_bilinear \ + -i "${image}" \ + -vf "scale=w=${CROP_WIDTH}:h=${CROP_HEIGHT}:force_original_aspect_ratio=increase,crop=${CROP_WIDTH}:${CROP_HEIGHT}" \ + "${cropped_image}" >/dev/null 2>&1 \ + && [ -f "${cropped_image}" ]; then + image="${cropped_image}" + fi + fi + fi + fi + + if ! [ -f "${image}" ]; then + echo "Error: Splash image not found at ${image}" + exit 1 + fi + + case "${video_rotation}" in + 1|2|3) + FBV_OPTS="${FBV_OPTS} -o ${video_rotation}" + echo "Applying FBV rotation: -o ${video_rotation}" + ;; + esac + + MODELOPTS="/etc/opts.${V_BOARD_MODEL}" + [ -e "${MODELOPTS}" ] && . "${MODELOPTS}" + + echo "Executing Command: fbv ${FBV_OPTS} \"${image}\"" + fbv ${FBV_OPTS} "${image}" >/dev/null 2>&1 +} + +do_stop () { + [ -e /dev/fb0 ] && dd if=/dev/zero of=/dev/fb0 >/dev/null 2>&1 +} + +case "$1" in + start) + do_start & + ;; + stop) + do_stop + ;; + *) + echo "Usage: $0 {start|stop}" + exit 1 + ;; +esac + +exit $? diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S06audio b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S06audio new file mode 100755 index 00000000000..ce6cbd3143c --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S06audio @@ -0,0 +1,106 @@ +#!/bin/sh +# +# Starts audio services +# + +. /etc/profile.d/xdg.sh +. /etc/profile.d/dbus.sh + +TAG="S06audio" +export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/var/run} + +log() { + local level="$1" + local msg="$2" + echo "[$TAG] $msg" + logger -t "$TAG" -p "user.$level" "$msg" +} + +start_pipewire() { + log "info" "Starting audio service sequence..." + + # Wait for the system D-Bus socket to exist before starting PipeWire. + # WirePlumber (spawned by PipeWire as its session manager) connects to + # this same socket as its "session" bus (see /etc/profile.d/dbus.sh). + # If PipeWire starts before dbus-daemon has created the socket, + # WirePlumber's D-Bus module fails once ("Cannot autolaunch D-Bus + # without X11 $DISPLAY") and never retries, permanently disabling its + # bluez5 monitor - confirmed live (no Bluetooth audio sink ever + # appears, even though pairing/connection succeed at the bluetoothd + # level) until WirePlumber is manually restarted after the socket + # exists. + N=0 + while [ ! -S "/run/dbus/system_bus_socket" ] && [ $N -lt 50 ]; do + sleep 0.1 + N=$((N+1)) + done + if [ ! -S "/run/dbus/system_bus_socket" ]; then + log "err" "D-Bus system socket still missing after 5s wait - WirePlumber's D-Bus module will likely fail to start" + fi + + # Cleanup stale locks + rm -f /var/run/pipewire-0 /var/run/pipewire-0.lock + # Start the PipeWire daemon + start-stop-daemon -S -b -q -m -p /var/run/pipewire.pid --exec /usr/bin/pipewire + if [ $? -eq 0 ]; then + log "info" "PipeWire daemon backgrounded (PID $(cat /var/run/pipewire.pid))" + else + log "err" "Failed to background PipeWire daemon" + return 1 + fi + # Wait loop for real hardware discovery + N=0 + MAX_RETRIES=50 + + while [ $N -lt $MAX_RETRIES ]; do + # Check through audio init stack + if [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then + REASON="Waiting for pipewire-0 socket..." + elif ! pactl info >/dev/null 2>&1; then + REASON="Pulse bridge initializing..." + elif ! sinks=$(pactl list sinks short 2>/dev/null) || ! echo "$sinks" | grep -qE "alsa_output|pci|usb"; then + REASON="Hardware discovery in progress (Sinks found: $(echo $sinks | awk '{print $2}' | xargs || echo 'none'))" + else + log "info" "Hardware sinks discovered: $(echo "$sinks" | grep -E 'alsa|pci|usb' | awk '{print $2}' | xargs)" + echo "OK" + return 0 + fi + # Continue to keep us informed... + if [ $((N % 20)) -eq 0 ]; then + log "info" "Status [${N}/50]: $REASON" + fi + sleep 0.1 + N=$((N+1)) + done + + log "err" "TIMEOUT: No hardware sinks found in 5s. Only dummy/null may be available." + echo "FAIL" + return 1 +} + +stop_pipewire() { + log "info" "Stopping audio services..." + start-stop-daemon -K -q -p /var/run/pipewire.pid + killall wireplumber pipewire-pulse pipewire 2>/dev/null + rm -f /var/run/pipewire.pid + echo "OK" +} + +case "$1" in + start) + alsactl init 2>&1 | logger -t "$TAG" -p "user.info" + start_pipewire + ;; + stop) + stop_pipewire + ;; + restart|reload) + $0 stop + sleep 1 + $0 start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 + ;; +esac diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S27audioconfig b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S27audioconfig new file mode 100755 index 00000000000..27cf7a09032 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S27audioconfig @@ -0,0 +1,128 @@ +#!/bin/bash +# +# Starts audio configuration +# + +. /etc/profile.d/xdg.sh +. /etc/profile.d/dbus.sh + +TAG="S27audioconfig" + +log() { + local level="$1" + local msg="$2" + echo "[$TAG] $msg" + logger -t "$TAG" -p "user.$level" "$msg" +} + +apply_audioconfig() { + DEVICE=$1 + PROFILE=$2 + VOLUME=$3 + EXITCODE=0 + + # Check Connection & Log available hardware for debugging + if ! pactl info >/dev/null 2>&1; then + log "err" "Cannot connect to PipeWire." + return 1 + fi + + CURRENT_SINKS=$(pactl list sinks short | awk '{print $2}' | xargs) + log "info" "Current available sinks: $CURRENT_SINKS" + + # Set Card Profile + if test -n "${PROFILE}" && [ "${PROFILE}" != "auto" ]; then + log "info" "Applying profile: ${PROFILE}" + ERR=$(batocera-audio set-profile "${PROFILE}" 2>&1) + [ $? -ne 0 ] && log "err" "Profile failed: $ERR" && EXITCODE=1 + fi + + # Set Default Sink + # "auto" is applied too (not just explicit device names): on boards + # where batocera-audio's "set auto" resolves to a board-specific + # default (e.g. rk3568/odroidm1 following the active display), skipping + # it here left the sink wherever PipeWire's own policy happened to put + # it, never actually applying that default at boot. + if [ "${DEVICE}" = "auto" ]; then + log "info" "Applying device: auto" + ERR=$(batocera-audio set auto 2>&1) + [ $? -ne 0 ] && log "err" "Set-device (auto) failed: $ERR" && EXITCODE=1 + elif test -n "${DEVICE}"; then + log "info" "Applying device: ${DEVICE}" + # Check if the device is actually in the list we just found + if [[ ! "$CURRENT_SINKS" =~ "$DEVICE" ]]; then + log "err" "Device '$DEVICE' not found in available sinks list." + EXITCODE=1 + else + ERR=$(batocera-audio set "${DEVICE}" 2>&1) + [ $? -ne 0 ] && log "err" "Set-device failed: $ERR" && EXITCODE=1 + fi + fi + + # Set Volume + log "info" "Applying volume: ${VOLUME}" + batocera-audio setSystemVolume "${VOLUME}" >/dev/null 2>&1 + + return ${EXITCODE} +} + +try_apply_audioconfig() { + DEVICE=$1 + PROFILE=$2 + VOLUME=$3 + N=10 + log "info" "Starting background retry loop..." + + while test "${N}" -gt 0; do + if apply_audioconfig "${DEVICE}" "${PROFILE}" "${VOLUME}"; then + log "info" "Audio configuration applied successfully." + return 0 + fi + log "info" "Retry loop: $((N-1)) attempts left..." + sleep 2 + let N-- + done + log "err" "Retry loop exhausted. Audio hardware never appeared." +} + +start_audioconfig() { + DEVICE=$(/usr/bin/batocera-settings-get-master audio.device) + PROFILE=$(/usr/bin/batocera-settings-get-master audio.profile) + VOLUME=$(/usr/bin/batocera-settings-get-master audio.volume) + + test -z "${DEVICE}" && DEVICE=auto + test -z "${PROFILE}" && PROFILE=auto + test -z "${VOLUME}" && VOLUME=80 + + log "info" "Targeting Device: $DEVICE" + + if ! apply_audioconfig "${DEVICE}" "${PROFILE}" "${VOLUME}"; then + log "err" "Initial attempt failed. Retrying in background..." + try_apply_audioconfig "${DEVICE}" "${PROFILE}" "${VOLUME}" & + else + echo "OK" + fi +} + +stop_audioconfig() { + VOLUME=$(batocera-audio getSystemVolume) + [ -e "/userdata/system/batocera.conf" ] && /usr/bin/batocera-settings-set audio.volume "${VOLUME}" + echo "OK" +} + +case "$1" in + start) + start_audioconfig + ;; + stop) + stop_audioconfig + ;; + restart|reload) + stop_audioconfig + start_audioconfig + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 + ;; +esac diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S28splash b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S28splash new file mode 100755 index 00000000000..04cd562433f --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/etc/init.d/S28splash @@ -0,0 +1,274 @@ +#!/bin/bash + +log="/userdata/system/logs/display.log" + +DAEMON="splash-mpv" +DEFAULT="/usr/share/batocera/splash/splash.mp4" +SPLASH_DIR=/userdata/splash + +FM_IMAGE=".*\.\(jpg\|jpeg\|png\)" +FM_VIDEO=".*\.\(mp4\)" + +. /etc/profile.d/xdg.sh +. /etc/profile.d/dbus.sh + +# CHA wants a specific splash file +if [ -f /sys/firmware/devicetree/base/model ]; then + IFS= read -r BOARD_MODEL > "$log" + drm_connector=$(echo "$status_file" | sed -n 's/.*\/card[0-9]\-\(.*\)\/status/\1/p') + echo "Splash: Selected DRM connector: $drm_connector" >> "$log" + card_number=$(echo "$card_id" | sed -n 's/card\([0-9]\+\).*/\1/p') + card_path="/dev/dri/card$card_number" + echo "Splash: DRM connected card path: $card_path" >> "$log" + break 2 # We found a connected display, exit both loops + fi + fi + done + fi + fi + done + + echo "$drm_connector $card_path" +} + +do_videostart () +{ + video="$1" + printf 'Video: %s: ' "$video" + mpv_audio=--ao=sdl + + soundDisabled=$(batocera-settings-get splash.screen.sound) + if [[ $? -eq 0 ]]; then + if test "${soundDisabled}" = 0 + then + mpv_audio=--no-audio + fi + fi + + ## current drm output ## + effectiveDrmOutput=$(for GPU in /dev/dri/card*; do batocera-drminfo "${GPU}" "current" 2>/dev/null | grep -oP '^[0-9]+\.[0-9]+:\K[^ ]+'; done | head -1) + + # Natively crop 16:9 on the GPU presentation plane. + mpv_video="--panscan=1.0" + + video_rotation=$(batocera-settings-get-master "display.rotate.${effectiveDrmOutput}") + if test -z "${video_rotation}" + then + video_rotation=$(batocera-settings-get-master display.rotate) + fi + if [[ $? -eq 0 ]]; then + video_rotation=$(do_angels_conversion $video_rotation) + # --blend-subtitles=video : rotation of subtitles + [[ $? -eq 0 ]] && mpv_video="${mpv_video} --video-rotate=${video_rotation} --blend-subtitles=video" + fi + + drm_mode="preferred" + video_resize=$(batocera-settings-get-master splash.screen.resize) + # If splash screen resize is empty then try the es resolution + if [[ -z "$video_resize" ]]; then + video_resize=$(batocera-settings-get-master es.resolution) + fi + if [[ -n "$video_resize" ]]; then + # avoid using --vf=scale with --blend-subtitles + # gpu-next should handle the scaling + drm_mode="${video_resize%%.*}" + fi + + video_subtitle=$(batocera-settings-get splash.screen.subtitle) + if [[ $? -eq 0 ]]; then + if test "${video_subtitle}" = 0 + then + mpv_video="${mpv_video} --no-sub-auto" + fi + fi + + DISPLAY_COMPOSITOR=$(/usr/bin/batocera-resolution getDisplayComp) + + # determine the output display to use for MPV + # fixes splash video output where the user wants it + if preferred_display=$(batocera-settings-get-master global.videooutput); then + + if [ "$DISPLAY_COMPOSITOR" = "labwc" ]; then + echo "Splash: Preferred display is $preferred_display" >> "$log" + else + echo "Splash: Preferred display is $preferred_display" > "$log" + fi + + for card_folder_path in /sys/class/drm/card*; do + # Construct the full path to the specific connector's status file + specific_connector_status_file="${card_folder_path}/${card_folder_path##*/}-${preferred_display}/status" + + if [ -f "$specific_connector_status_file" ] && [ "$(cat "$specific_connector_status_file")" == "connected" ]; then + echo "Splash: Exactly matched preferred display '$preferred_display' and it is connected." >> "$log" + drm_connector="$preferred_display" + card_id=$(basename "$card_folder_path") + card_number=$(echo "$card_id" | sed -n 's/card\([0-9]\+\).*/\1/p') + card_path="/dev/dri/card$card_number" + echo "Splash: Selected DRM connector: $drm_connector" >> "$log" + echo "Splash: DRM connected card path: $card_path" >> "$log" + break # Found our exact match, exit this loop + fi + done + + if [ -z "$drm_connector" ]; then + echo "Splash: Exact preferred display not found or not connected. Falling back to original method." >> "$log" + display_type=$(echo "$preferred_display" | cut -d'-' -f1) + if [[ "$display_type" == "DisplayPort" ]]; then + # workaround some cards using DisplayPort as the xorg output name + display_type="DP" + fi + echo "Splash: Display Type = $display_type" >> "$log" + read -r drm_connector card_path <<< "$(get_connected_display "$display_type")" + fi + else + # we choose the first connected display + mpv_connector=$(mpv --drm-connector=help | grep "(connected)" | sed 's/ (connected)//' | sed 's/^[[:space:]]*//' | head -n1) + if [ -n "$mpv_connector" ]; then + if [ "$DISPLAY_COMPOSITOR" = "labwc" ]; then + echo "Splash: No preferred display chosen, using the first MPV detected (connected) display: $mpv_connector" >> "$log" + else + echo "Splash: No preferred display chosen, using the first MPV detected (connected) display: $mpv_connector" > "$log" + fi + + display_type=$(echo "$mpv_connector" | cut -d'-' -f1) + echo "Splash: MPV detected display Type = $display_type" >> "$log" + read -r drm_connector card_path <<< "$(get_connected_display "$display_type")" + else + if [ "$DISPLAY_COMPOSITOR" = "labwc" ]; then + echo "Splash: No preferred display chosen, no connected display found" >> "$log" + else + echo "Splash: No preferred display chosen, no connected display found" > "$log" + fi + fi + fi + + # On some systems, the graphics driver may take an extra moment to become fully + # ready after detection. Check for a user-configurable delay. + splash_delay=$(batocera-settings-get splash.delay) + + # Check if the setting was found and is a number greater than 0 + if [[ "${splash_delay}" =~ ^[0-9]+$ ]] && [ "${splash_delay}" -gt 0 ]; then + sleep "${splash_delay}" + fi + + # Render OSD and subtitles directly from memory to bypass slow, early boot fontconfig scans + local mpv_fast_fonts=( + "--sub-font-provider=none" + "--osd-font-provider=none" + "--sub-fonts-dir=/usr/share/fonts/dejavu/" + "--sub-font=DejaVu Sans" + ) + + if [ -n "$drm_connector" ] && [ -n "$card_path" ]; then + start-stop-daemon -S -b -q -m -p /var/run/user-splash.pid --exec /usr/bin/mpv -- --really-quiet --no-config %PLAYER_OPTIONS% "${mpv_fast_fonts[@]}" --drm-mode=$drm_mode --drm-connector=$drm_connector --drm-device=$card_path --log-file=/userdata/system/logs/mpv.log $mpv_audio $mpv_video "$video" + else + start-stop-daemon -S -b -q -m -p /var/run/user-splash.pid --exec /usr/bin/mpv -- --really-quiet --no-config %PLAYER_OPTIONS% "${mpv_fast_fonts[@]}" --drm-mode=$drm_mode --log-file=/userdata/system/logs/mpv.log $mpv_audio $mpv_video "$video" + fi +} + +case "$1" in + start) + printf 'Starting %s: ' "$DAEMON" + + # set the sound again due to drm init causing some cards to default to + # 40% volume. Moved out of do_start() (called below, only when the + # splash video actually plays) - splash.screen.enabled is never "1" + # on a genuinely fresh SD's first boot (see batocera-boot.conf sync + # timing elsewhere in this project), so on that boot do_start() never + # runs at all and this never applied either. This must run on every + # invocation of this script regardless of whether the video plays. + VOLUME=$(/usr/bin/batocera-settings-get audio.volume) + batocera-audio setSystemVolume "${VOLUME}" + + # ODROID-M1 rk809 DAC hardware volume gets the same treatment: + # soundconfig (udev-triggered on the sound card's own probe, well + # before PipeWire starts) already sets it to 252/255 - the highest + # value the driver actually accepts, see soundconfig's own comment - + # but PipeWire/ACP claiming the card later resets it back to the + # driver's power-on default (~80%, -20dB), same timing as the volume + # reset above just affecting the ALSA mixer element PipeWire's own + # volume sits on top of instead of PipeWire's software volume itself. + if [ "$(cat /boot/boot/batocera.board 2>/dev/null)" = "bsp-odroidm1" ]; then + rk809_card=$(awk '/rockchiprk809co/ {print $1; exit}' /proc/asound/cards) + [ -n "${rk809_card}" ] && amixer -c "${rk809_card}" sset DAC 252,252 >/dev/null 2>&1 + fi + + if grep -qE '^[ ]*splash.screen.enabled[ ]*=[ ]*1[ ]*$' "/boot/batocera-boot.conf" + then + do_start + echo "OK" + else + echo "SKIPPED" + fi + ;; + stop) + start-stop-daemon -K -q -p /var/run/user-splash.pid + ;; + restart|reload) + ;; + *) +esac + +exit $? diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-audio b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-audio new file mode 100755 index 00000000000..09d03d2d4cc --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-audio @@ -0,0 +1,189 @@ +#!/bin/bash + +export LC_ALL=C + +do_usage() { + echo "$1 list" + echo "$1 get" + echo "$1 set {auto|}" + echo "$1 list-profiles" + echo "$1 get-profile" + echo "$1 set-profile {auto|}" + echo "$1 getSystemVolume" + echo "$1 setSystemVolume {mute|unmute|mute-toggle|[+-]<0...100>}" + echo "$1 getSystemMute" + echo "$1 test" +} + +# Export XDG for PW +export XDG_RUNTIME_DIR=/var/run + +# Set variables +MAX_VOLUME="$(/usr/bin/batocera-settings-get audio.volume.boost)" || MAX_VOLUME=100 +DEFAULT_SINK=$(LC_ALL=C pactl info | grep -E '^Default Sink: ' | sed -e s+"^Default Sink: "+""+ | head -1) +DETECT_VOLUME=$(LC_ALL=C pactl list sinks-raw | grep -E '^[^ ]* name="'"${DEFAULT_SINK}"'"' | sed -e s+"^.* volume=\"\([0-9]*\)\" .*$"+'\1'+) + +ACTION="$1" + +case "${ACTION}" in + list) + printf "auto\tauto\n" + LC_ALL=C pactl list sinks-raw | sed -e s+"^sink=\"[0-9]*\" name=\"\([^ ]*\)\" .* description=\"\(.*\)\"$"+"\1\t\2"+ + ;; + + get) + /usr/bin/batocera-settings-get audio.device + ;; + + set) + MODE="$2" + case "${MODE}" in + auto) + BT_SINK=$(LC_ALL=C pactl list short sinks | grep bluez_output | awk '{print $2}' | head -n1) + if [ -n "$BT_SINK" ]; then + MODE="$BT_SINK" + LC_ALL=C pactl set-default-sink "${MODE}" || exit 1 + else + ARCH=$(cat /usr/share/batocera/batocera.arch) + BOARD=$(cat /boot/boot/batocera.board) + + case "${ARCH}" in + "rk3399") + MODE=alsa_output.platform-hdmi-sound.stereo-fallback + LC_ALL=C pactl set-default-sink "${MODE}" || exit 1 + ;; + "s922x") + if [[ "$BOARD" == "ogu" ]]; then + LC_ALL=C pactl set-default-sink INTERNAL + amixer -c 0 -q sset "Master" 100% + amixer -c 0 sset 'Playback Mux' SPK + amixer -c 0 -q sset 'FRDDR_A SINK 1 SEL' 'OUT 1' + amixer -c 0 -q sset 'FRDDR_A SRC 1 EN' 'on' + fi + ;; + "rk3326") + LC_ALL=C pactl set-default-sink INTERNAL + amixer -c 0 cset name='Playback Path' SPK + ;; + "rk3568") + # "rk3568" is the shared arch for every board in + # this family (rock-3a, several anbernic/orangepi + # boards, ...), not just ours - the rk809 codec + # and the HDMI sink's sysfs-path-based name below + # are specific to the ODROID-M1's hardware/DTB + # and don't hold for the others (no rk809 sound + # card there, HDMI sink name likely differs). + # Scope to our board via batocera.board (same + # mechanism the "ogu" check above uses) so this + # doesn't break `set auto` on every other rk3568 + # board. + if [[ "$BOARD" == "bsp-odroidm1" ]]; then + # Follow HDMI's own DRM hotplug status rather + # than whether the DSI/VU8M overlay happened + # to be baked into this boot's DTB (config.ini's + # overlays= - see boot.cmd). DSI panels have no + # hotplug detection, so "DSI-1 present in DRM" + # only ever reflected a build-time choice, not + # whether a cable is actually plugged in - a + # boot with no VU8M overlay and no HDMI cable + # connected would still get routed to HDMI and + # produce no sound. The internal speaker needs + # no such check: it's a fixed onboard + # component, always present whenever the board + # is powered, so it's the correct default + # whenever HDMI isn't actively connected. + if modetest -c 2>/dev/null | awk -F'\t' '$4 ~ /^HDMI-A-1/ {print $3}' | grep -qx connected; then + MODE=alsa_output._sys_devices_platform_hdmi-sound_sound_card1.stereo-fallback + else + MODE=alsa_output._sys_devices_platform_rk809-sound_sound_card0.stereo-fallback + fi + LC_ALL=C pactl set-default-sink "${MODE}" || exit 1 + fi + ;; + *) + # do nothing! + ;; + esac + fi + ;; + *) + LC_ALL=C pactl set-default-sink "${MODE}" || exit 1 + if [[ $(LC_ALL=C pactl get-default-sink) != "${MODE}" ]]; then + # default-sink didn't switch right away for some reason, try again later + exit 1 + fi + ;; + esac + ;; + + list-profiles) + printf "auto\tauto\n" + LC_ALL=C pactl list cards-profiles-raw | grep 'available="1"' | sed -E 's/^card="[0-9]+" name="(.*)" profile="(.*)" available="[01]" active="[01]" description="(.*)"$/\2@\1\t\3/' + ;; + + get-profile) + /usr/bin/batocera-settings-get audio.profile + ;; + + set-profile) + MODE="$2" + if [[ "${MODE}" != "auto" ]]; then + if batocera-audio list-profiles | grep -q "${MODE}"; then + PROFILE_NAME=$(echo "${MODE}" | cut -d '@' -f 1) + CARD_NAME=$(echo "${MODE}" | cut -d '@' -f 2) + LC_ALL=C pactl set-card-profile "${CARD_NAME}" "${PROFILE_NAME}" || exit 1 + else + # the profile isn't available yet, try again later + exit 1 + fi + fi + ;; + + getSystemVolume) + echo "${DETECT_VOLUME}" + ;; + + getSystemMute) + LC_ALL=C pactl get-sink-mute @DEFAULT_SINK@ | sed 's/.*: *\(.*\)/\1/' || exit 1 + ;; + + setSystemVolume) + VOLUME="$2" + case "${VOLUME}" in + "mute") + LC_ALL=C pactl set-sink-mute @DEFAULT_SINK@ 1 || exit 1 + ;; + "unmute") + LC_ALL=C pactl set-sink-mute @DEFAULT_SINK@ 0 || exit 1 + ;; + "mute-toggle") + LC_ALL=C pactl set-sink-mute @DEFAULT_SINK@ toggle || exit 1 + ;; + *) + if echo "${VOLUME}" | grep -qE '^[0-9]+$'; then + if [[ "${VOLUME}" -ge 0 && "${VOLUME}" -le "${MAX_VOLUME}" ]]; then + LC_ALL=C pactl set-sink-volume @DEFAULT_SINK@ "${VOLUME}%" || exit 1 + echo "${VOLUME}" > /var/run/batocera-pending-volume + (sleep 2 && VOL=$(cat /var/run/batocera-pending-volume 2>/dev/null) && [ -n "${VOL}" ] && /usr/bin/batocera-settings-set audio.volume "${VOL}") & + exit 0 + fi + elif echo "${VOLUME}" | grep -qE '^[+-][0-9]+$'; then + let NEW_VOLUME=${DETECT_VOLUME}${VOLUME} + NEW_VOLUME=$(( NEW_VOLUME < 0 ? 0 : (NEW_VOLUME > MAX_VOLUME ? MAX_VOLUME : NEW_VOLUME) )) + LC_ALL=C pactl set-sink-volume @DEFAULT_SINK@ "${NEW_VOLUME}%" || exit 1 + echo "${NEW_VOLUME}" > /var/run/batocera-pending-volume + (sleep 2 && VOL=$(cat /var/run/batocera-pending-volume 2>/dev/null) && [ -n "${VOL}" ] && /usr/bin/batocera-settings-set audio.volume "${VOL}") & + exit 0 + fi + echo "invalid volume value" >&2 + exit 1 + ;; + esac + ;; + + test) + aplay "/usr/share/sounds/Mallet.wav" || exit 1 + ;; + *) + do_usage "$(basename "$0")" +esac diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-battery-checker b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-battery-checker new file mode 100755 index 00000000000..a81dbf7dfff --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-battery-checker @@ -0,0 +1,84 @@ +#!/bin/bash + +export LC_ALL=C + +# Function to get the battery percentage +getBattery() { + BATT=$(cat /sys/class/power_supply/*{BAT,bat}*/uevent 2>/dev/null | grep -E "^POWER_SUPPLY_CAPACITY=" | sed -e 's/^POWER_SUPPLY_CAPACITY=//' | sort -rn | head -1) + if ! test -n "${BATT}"; then + NOW=$(cat /sys/class/power_supply/*{FUEL,fuel}*/uevent 2>/dev/null | grep -E "^POWER_SUPPLY_CHARGE_NOW=" | sed -e 's/^POWER_SUPPLY_CHARGE_NOW=//' | sort -rn | head -1) + MAX=$(cat /sys/class/power_supply/*{FUEL,fuel}*/uevent 2>/dev/null | grep -E "^POWER_SUPPLY_CHARGE_FULL=" | sed -e 's/^POWER_SUPPLY_CHARGE_FULL=//' | sort -rn | head -1) + if [ ! -z "$NOW" ] && [ ! -z "$MAX" ] && [ "$MAX" != 0 ]; then + BATT=$((200 * $NOW / $MAX % 2 + 100 * $NOW / $MAX)) + fi + fi + echo ${BATT} +} + +# Function to check if the ayn power supply is connected +is_power_connected() { + if compgen -G "/sys/class/power_supply/*/online" > /dev/null; then + PLUGGED=$(cat /sys/class/power_supply/*/online 2>/dev/null | grep -E "^1") + else + # No power_supply class devices at all (no battery, no AC "online" + # node - e.g. desktop-style SBCs like the ODROID-M1) - nothing for + # the glob above to match, which used to fall through to "not + # connected" and made this checker call `batocera-power-mode + # battery` right at boot on every such board, silently overriding + # whatever CPU governor S18governor had just applied (confirmed + # live: interactive correctly set by S18governor, then reset to + # schedutil by this call ~10s later). Treat "no power_supply info + # at all" as always AC-connected instead. + PLUGGED="1" + fi + if [ -n "${PLUGGED}" ]; then + echo 1 # Power supply is connected + else + echo 0 # Power supply is not connected + fi +} + +# Initial power state +POWER_CONNECTED=$(is_power_connected) +if [[ "$POWER_CONNECTED" == 0 ]]; then + /usr/bin/batocera-power-mode battery +fi + +LAST_BATTERY=100 +while true; do + NEW_BATTERY=$(getBattery) + DELAY=4 + CURRENT_POWER_CONNECTED=$(is_power_connected) + + # Check if power state has changed + if [[ "$CURRENT_POWER_CONNECTED" != "$POWER_CONNECTED" ]]; then + if [[ "$CURRENT_POWER_CONNECTED" == 0 ]]; then + /usr/bin/batocera-power-mode battery + else + /usr/bin/batocera-power-mode ac + fi + POWER_CONNECTED="$CURRENT_POWER_CONNECTED" + fi + + if [[ "$CURRENT_POWER_CONNECTED" == 0 ]]; then + # Power not connected + if test -z "${NEW_BATTERY}"; then + exit 0 + fi + if test "${NEW_BATTERY}" != "${LAST_BATTERY}"; then + if test "${NEW_BATTERY}" -le 30; then + if test "${NEW_BATTERY}" -lt "${LAST_BATTERY}"; then + if test "${NEW_BATTERY}" -le 10 -o $((${NEW_BATTERY} / 10)) -lt $((${LAST_BATTERY} / 10)); then + if test "${NEW_BATTERY}" -le 5; then + DELAY=10 + fi + LANG=en_US HOME=/userdata/system XAUTHORITY=/var/lib/.Xauthority DISPLAY=$(getLocalXDisplay) /usr/bin/batocera-flash-screen ${DELAY} red "$(/usr/bin/batocera-info --short)" + fi + fi + fi + fi + fi + + LAST_BATTERY=${NEW_BATTERY} + sleep 5 # do a check every 5 seconds +done diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-power-mode b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-power-mode new file mode 100755 index 00000000000..5535735c8e1 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/batocera-power-mode @@ -0,0 +1,190 @@ +#!/bin/bash + +if ! { [ -e /sys/devices/system/cpu/cpufreq/policy0/scaling_governor ] && + [ -e /sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors ]; }; then + exit 0 +fi + +EPP_AVAILABLE="" +if [ -e /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences ]; then + EPP_AVAILABLE="1" +else + EPP_AVAILABLE="0" +fi + +# Check if governor exists for CPU +check_governor() { + local GOVERNOR_TO_CHECK=$1 + local AVAILABLE_GOVERNORS + AVAILABLE_GOVERNORS=$(cat /sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors) + + if [[ " $AVAILABLE_GOVERNORS " =~ [[:space:]]${GOVERNOR_TO_CHECK}[[:space:]] ]]; then + return 0 + else + return 1 + fi +} + +# Set governor +set_governor() { + local GOVERNOR_NAME=$1 + + # Apply the governor to all policies + for policy in /sys/devices/system/cpu/cpufreq/policy*; do + if [ -e "$policy/scaling_governor" ]; then + local CURRENT_GOVERNOR=$(cat "$policy/scaling_governor") + if [ "$CURRENT_GOVERNOR" != "$GOVERNOR_NAME" ]; then + echo $GOVERNOR_NAME > "$policy/scaling_governor" 2>/dev/null + fi + fi + done +} + +# Set GPU devfreq governor, if one exists (name-matched, not hardcoded to a +# specific SoC's devfreq node - e.g. Rockchip's is "fde60000.gpu", but this +# also covers other boards whose GPU devfreq node happens to be named with +# "gpu" in it). Falls back silently (no-op) if the requested governor isn't +# in that node's available list, or there's no GPU devfreq node at all - a +# board without devfreq or with a differently-named node just isn't affected. +set_gpu_governor() { + local GOVERNOR_NAME=$1 + + for devfreq in /sys/class/devfreq/*gpu*; do + [ -e "$devfreq/governor" ] || continue + local AVAILABLE + AVAILABLE=$(cat "$devfreq/available_governors" 2>/dev/null) + if [[ " $AVAILABLE " =~ [[:space:]]${GOVERNOR_NAME}[[:space:]] ]]; then + local CURRENT + CURRENT=$(cat "$devfreq/governor" 2>/dev/null) + if [ "$CURRENT" != "$GOVERNOR_NAME" ]; then + echo "$GOVERNOR_NAME" > "$devfreq/governor" 2>/dev/null + fi + fi + done +} + +# Check if requested EPP is available +check_epp() { + local PREFERENCE=$1 + local AVAILABLE_PREFERENCES + AVAILABLE_PREFERENCES=$(cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences) + + # Check if the specified preference is available + if [[ " $AVAILABLE_PREFERENCES " =~ [[:space:]]${PREFERENCE}[[:space:]] ]]; then + return 0 + else + return 1 + fi +} + +# Set Energy Performance Preference +set_epp() { + local PREFERENCE=$1 + + for cpu in /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference; do + if [ -e "$cpu" ]; then + echo "$PREFERENCE" > "$cpu" 2>/dev/null + fi + done +} + +# Set system default +set_default() { + SYSTEM_GOVERNOR="$(/usr/bin/batocera-settings-get-master system.cpu.governor)" + SYSTEM_GPU_GOVERNOR="$(/usr/bin/batocera-settings-get-master system.gpu.governor)" + set_gpu_governor "${SYSTEM_GPU_GOVERNOR:-simple_ondemand}" + + if check_governor "$SYSTEM_GOVERNOR"; then + set_governor "$SYSTEM_GOVERNOR" + # If EPP available set to default or performance as fallback + if [ "$EPP_AVAILABLE" -eq 1 ]; then + if check_epp "default"; then + set_epp "default" + elif check_epp "performance"; then + set_epp "performance" + fi + fi + fi +} + +# Determine which Governors & if applicable EPP to set based on powermode setting +handle_powermode() { + local POWERMODE_NAME=$1 + + case "$POWERMODE_NAME" in + "highperformance") + set_governor "performance" + set_gpu_governor "performance" + if [ "$EPP_AVAILABLE" -eq 1 ]; then + if check_epp "performance"; then + set_epp "performance" + fi + fi + ;; + "balanced") + # Handle governor + if check_governor "schedutil"; then + set_governor "schedutil" + elif check_governor "ondemand"; then + set_governor "ondemand" + elif check_governor "conservative"; then + set_governor "conservative" + elif check_governor "powersave"; then + set_governor "powersave" + fi + set_gpu_governor "simple_ondemand" + + # Handle EPP + if [ "$EPP_AVAILABLE" -eq 1 ]; then + if check_epp "default"; then + set_epp "default" + elif check_epp "performance"; then + set_epp "performance" + fi + fi + ;; + "powersaver") + set_governor "powersave" + set_gpu_governor "powersave" + if [ "$EPP_AVAILABLE" -eq 1 ]; then + if check_epp "power"; then + set_epp "power" + fi + fi + ;; + *) + ;; + esac +} + +case "$1" in + ac) + POWER_MODE=$(cat /var/tmp/power_mode 2>/dev/null) + if [ -z "$POWER_MODE" ]; then + POWER_MODE="$(/usr/bin/batocera-settings-get-master global.powermode)" + if [ -z "${POWER_MODE}" ]; then + set_default + fi + fi + handle_powermode "$POWER_MODE" + ;; + battery) + POWER_MODE="$(/usr/bin/batocera-settings-get-master global.batterymode)" + if [ -z "$POWER_MODE" ]; then + POWER_MODE="balanced" + fi + handle_powermode "$POWER_MODE" + ;; + highperformance|balanced|powersaver) + handle_powermode "$1" + ;; + default) + set_default + ;; + *) + echo "Invalid input. Usage: $0 [ac|battery|default|highperformance|balanced|powersaver]" + exit 1 + ;; +esac + +exit 0 diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/soundconfig b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/soundconfig new file mode 100755 index 00000000000..501ec115509 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/bin/soundconfig @@ -0,0 +1,178 @@ +#!/bin/sh + +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2009-2014 Stephan Raue (stephan@openelec.tv) +# Copyright (C) 2018-present Team LibreELEC (https://libreelec.tv) + +# setup alsa (especially the mixer config) + +mixer() { + parm=${4:-on} + amixer -c "$1" sset "$2" "$3" $parm >/dev/null 2>&1 + amixer -c "$1" sset "$2" $parm >/dev/null 2>&1 +} + +( + +# get card num +card=`echo $1 | sed 's/[^0-9]*//g'` + +# set common mixer params +mixer $card Master 0db +mixer $card Front 100% +mixer $card PCM 0db +mixer $card Synth 100% + +# mute CD, since using digital audio instead +mixer $card CD 0% mute + +# Only unmute Line and Aux if they are possibly used. +# mixer $card Line 100% +# mixer $card Aux 100% + +# mute mic +mixer $card Mic 0% mute + +# ESS 1969 chipset has 2 PCM channels +mixer $card PCM,1 100% + +# Trident/YMFPCI/emu10k1 +mixer $card Wave 100% +mixer $card Music 100% +mixer $card AC97 100% +mixer $card Surround 90% +mixer $card 'Surround Digital' 90% +mixer $card 'Wave Surround' 90% +mixer $card 'Duplicate Front' 90% +mixer $card 'Sigmatel 4-Speaker Stereo' 90% + +# CS4237B chipset: +mixer $card 'Master Digital' 100% + +# DRC +mixer $card 'Dynamic Range Compression' 90% + +# Envy24 chips with analog outs +mixer $card DAC 100% +mixer $card DAC,0 100% +mixer $card DAC,1 100% + +# some notebooks use headphone instead of master +mixer $card Headphone 100% +mixer $card Speaker 100% +mixer $card 'Internal Speaker' 0% mute +mixer $card Playback 100% +mixer $card Headphone 100% +mixer $card Speaker 100% +mixer $card Center 100% +mixer $card LFE 100% +mixer $card Center/LFE 100% + +# Intel P4P800-MX (Ubuntu bug #5813) +mixer $card 'Master Playback Switch' on + +# set digital output mixer params +mixer $card 'IEC958' 100% on +mixer $card 'IEC958 Output' 100% +mixer $card 'IEC958 Coaxial' 100% +mixer $card 'IEC958 LiveDrive' 100% +mixer $card 'IEC958 Optical Raw' 100% +mixer $card 'SPDIF Out' 100% +mixer $card 'SPDIF Front' 100% +mixer $card 'SPDIF Rear' 100% +mixer $card 'SPDIF Center/LFE' 100% +mixer $card 'Master Digital' 100% + +mixer $card 'Analog Front' 100% +mixer $card 'Analog Rear' 100% +mixer $card 'Analog Center/LFE' 100% + +# ASRock ION 330 (and perhaps others) has 2 IEC958 channels +mixer $card IEC958,0 on +mixer $card IEC958,1 on + +# some ION2 has much more IEC958 channels ... +mixer $card IEC958,2 on +mixer $card IEC958,3 on + +# ASRock ION 330 has Master Front set to 0 +mixer $card 'Master Front' 100% + +# Shuttle XS35GT needs this too +mixer $card 'Master',0 100% on + +# and this for various Fusion devices like Zotac ZBOX +mixer $card 'Front',0 100% on + +# NVidia CK804 sound devices +mixer $card 'IEC958 Playback AC97-SPSA' 100% + +# Allwinner H3 Analog +mixer $card 'Line Out' 0db on + +# Allwinner A20 Analog +mixer $card 'Power Amplifier' 0db +mixer $card 'Power Amplifier DAC' on +mixer $card 'Power Amplifier Mute' on + +# Allwinner A64 Analog +mixer $card Headphone 0db on +mixer $card 'AIF1 Slot 0 Digital DAC' on + +BOARD=$(cat /boot/boot/batocera.board) +# Amlogic G12 HDMI +case ${BOARD} in + *vim3*) #TDM_A + mixer $card 'FRDDR_A SINK 1 SEL' 'OUT 0' + mixer $card 'FRDDR_A SRC 1 EN' on + mixer $card 'TDMOUT_A SRC SEL' 'IN 0' + mixer $card 'TOHDMITX' on + mixer $card 'TOHDMITX I2S SRC' 'I2S A' + ;; + *) #TDM_B + mixer $card 'FRDDR_A SINK 1 SEL' 'OUT 1' + mixer $card 'FRDDR_A SRC 1 EN' on + mixer $card 'TDMOUT_B SRC SEL' 'IN 0' + mixer $card 'TOHDMITX' on + mixer $card 'TOHDMITX I2S SRC' 'I2S B' + ;; + esac + +# Amlogic G12 S/PDIF +mixer $card 'FRDDR_B SINK 1 SEL' 'OUT 3' +mixer $card 'FRDDR_B SRC 1 EN' on +mixer $card 'SPDIFOUT SRC SEL' 'IN 1' +mixer $card 'SPDIFOUT Playback' on + +# Amlogic GX HDMI and S/PDIF +mixer $card 'AIU HDMI CTRL SRC' 'I2S' +mixer $card 'AIU SPDIF SRC SEL' 'SPDIF' + +# Amlogic S812 +mixer $card 'HDMI CTRL SRC' 'I2S' + +# Odroid Go Advance Speaker +mixer $card 'Playback Path' 'SPK' + +# ODROID-M1 rk817-codec (RK809 PMIC audio): the ALSA control reports +# "Limits: Playback 0 - 255" but the kernel driver only actually accepts +# up to 252 (confirmed via dmesg: "rk817_dac_vol_put: Volume out of range +# [0, 252]"). The generic "mixer $card DAC 100%" above silently fails on +# this board since 100% maps to the (invalid) top of the reported range, +# leaving playback stuck at whatever the driver's power-on default is +# (~80%, -20dB) regardless of what PipeWire's own volume shows. Called +# directly (not via the mixer() helper) since DAC is volume-only - the +# helper's default trailing "on" switch arg makes amixer reject the whole +# command, confirmed live with `bash -x`. +case ${BOARD} in + *odroidm1*) + amixer -c $card sset DAC 252,252 >/dev/null 2>&1 + ;; +esac + +# ES8316 headphone jack +mixer $card 'Right Headphone Mixer Right DAC' on +mixer $card 'Left Headphone Mixer Left DAC' on + +exit 0 +)& diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/duckstation/duckstationGenerator.py b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/duckstation/duckstationGenerator.py new file mode 100644 index 00000000000..c51e352a82e --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/duckstation/duckstationGenerator.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +from os import environ +from pathlib import Path +from typing import TYPE_CHECKING + +from batocera_common.configparser import CaseSensitiveConfigParser + +from ... import Command +from ...batoceraPaths import ( + BIOS, + CACHE, + CHEATS, + CONFIGS, + SAVES, + SCREENSHOTS, + ensure_parents_and_open, + mkdir_if_not_exists, +) +from ...controller import generate_sdl_game_controller_config, write_sdl_controller_db +from ...exceptions import BatoceraException +from ..Generator import Generator + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from ...types import HotkeysContext + + +class DuckstationGenerator(Generator): + + def getHotkeysContext(self) -> HotkeysContext: + return { + "name": "duckstation", + "keys": { "exit": ["KEY_LEFTALT", "KEY_F4"], "menu": "KEY_F7", "reset": "KEY_F6", + "restore_state": "KEY_F1", "save_state": "KEY_F2", "previous_slot": "KEY_F3", "next_slot": "KEY_F4", + "rewind": "KEY_F5", "fastforward": "KEY_TAB", "next_disk": "KEY_F8" + } + } + + def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution): + # Test if it's a m3u file + if rom.suffix == ".m3u": + rom = rewriteM3uFullPath(rom) + + if Path('/usr/bin/duckstation-qt').exists(): + commandArray = ["duckstation-qt", "-batch", "-nogui", "--", rom ] + else: + commandArray = ["duckstation-nogui", "-batch", "-fullscreen", "--", rom ] + + settings = CaseSensitiveConfigParser(interpolation=None) + settings_path = CONFIGS / "duckstation" / "settings.ini" + if settings_path.exists(): + settings.read(settings_path) + + ## [Main] + if not settings.has_section("Main"): + settings.add_section("Main") + # Settings, Language and ConfirmPowerOff + settings.set("Main", "SettingsVersion", "3") # Probably to be updated in the future + settings.set("Main", "InhibitScreensaver", "true") + settings.set("Main", "StartPaused", "false") + # Force Fullscreen + settings.set("Main", "StartFullscreen", "true") + settings.set("Main", "PauseOnFocusLoss", "false") + settings.set("Main", "PauseOnMenu", "true") + settings.set("Main", "ConfirmPowerOff", "false") + # Force applying game Settings fixes + settings.set("Main","ApplyGameSettings", "true") + # Remove wizard + settings.set("Main","SetupWizardIncomplete", "false") + # overclock + settings.set("Main","EmulationSpeed", system.config.get("duckstation_clocking", "1")) + # host refresh rate + settings.set("Main","SyncToHostRefreshRate", system.config.get("duckstation_hrr", "false")) + + # Rewind + #if system.config.get_bool('rewind'): + settings.set("Main","RewindEnable", "true") + settings.set("Main","RewindFrequency", "1") # Frame skipped each seconds + match system.config.get("duckstation_rewind"): + case '120' | '90' | '60' | '30' | '15' as rewind: + settings.set("Main","RewindSaveSlots", rewind) # Total duration available in sec + case '10': + settings.set("Main","RewindSaveSlots", "100") + settings.set("Main","RewindFrequency", "0.100000") + case '5': + settings.set("Main","RewindSaveSlots", "50") + settings.set("Main","RewindFrequency", "0.050000") + case _: + settings.set("Main","RewindEnable", "false") + # Discord + settings.set("Main","EnableDiscordPresence", "false") + # Language + settings.set("Main", "Language", getLangFromEnvironment()) + + ## [ControllerPorts] + if not settings.has_section("ControllerPorts"): + settings.add_section("ControllerPorts") + settings.set("ControllerPorts", "ControllerSettingsMigrated", "true") + settings.set("ControllerPorts", "MultitapMode", "Disabled") + settings.set("ControllerPorts", "PointerXScale", "8") + settings.set("ControllerPorts", "PointerYScale", "8") + settings.set("ControllerPorts", "PointerXInvert", "false") + settings.set("ControllerPorts", "PointerYInvert", "false") + + ## [Console] + if not settings.has_section("Console"): + settings.add_section("Console") + # Region + settings.set("Console", "Region", system.config.get("duckstation_region", "Auto")) + # Enable Cheats + settings.set("Console", "EnableCheats", system.config.get("duckstation_cheats", "False")) + + + ## [BIOS] + if not settings.has_section("BIOS"): + settings.add_section("BIOS") + settings.set("BIOS", "SearchDirectory", "/userdata/bios") + # Boot Logo + settings.set("BIOS", "PatchFastBoot", system.config.get("duckstation_PatchFastBoot", "false")) + # Find & populate BIOS + found_bios = find_bios(bios_lists) + + if not found_bios: + raise BatoceraException("No PSX1 BIOS found") + + # Set BIOS paths + if "Uni" in found_bios: + uni_bios = found_bios["Uni"] + settings.set("BIOS", "PathNTSCU", uni_bios) + settings.set("BIOS", "PathPAL", uni_bios) + settings.set("BIOS", "PathNTSCJ", uni_bios) + else: + region_mapping = {"NTSCU": "PathNTSCU", "PAL": "PathPAL", "NTSCJ": "PathNTSCJ"} + for region, bios in found_bios.items(): + settings.set("BIOS", region_mapping[region], bios) + + ## [CPU] + if not settings.has_section("CPU"): + settings.add_section("CPU") + # ExecutionMode + settings.set("CPU", "ExecutionMode", system.config.get("duckstation_executionmode", "Recompiler")) + + ## [GPU] + if not settings.has_section("GPU"): + settings.add_section("GPU") + # Renderer + settings.set("GPU", "Renderer", system.config.get("duckstation_gfxbackend", "OpenGL")) + # Multisampling force (MSAA or SSAA) - no GUI option anymore... + settings.set("GPU", "PerSampleShading", "false") + settings.set("GPU", "Multisamples", "1") + # Threaded Presentation (Vulkan Improve) + settings.set("GPU", "ThreadedPresentation", system.config.get("duckstation_threadedpresentation", "false")) + # Internal resolution + settings.set("GPU", "ResolutionScale", system.config.get("duckstation_resolution_scale", "1")) + # WideScreen Hack + settings.set("GPU", "WidescreenHack", system.config.get("duckstation_widescreen_hack", "false")) + # Force 60hz + settings.set("GPU", "ForceNTSCTimings", system.config.get("duckstation_60hz", "false")) + # TextureFiltering + settings.set("GPU", "TextureFilter", system.config.get("duckstation_texture_filtering", "Nearest")) + # PGXP - enabled by default + pgxp = system.config.get("duckstation_pgxp", "true") + settings.set("GPU", "PGXPEnable", pgxp) + settings.set("GPU", "PGXPCulling", pgxp) + settings.set("GPU", "PGXPTextureCorrection", pgxp) + settings.set("GPU", "PGXPPreserveProjFP", pgxp) + # True Color + settings.set("GPU", "TrueColor", system.config.get("duckstation_truecolour", "false")) + # Scaled Dithering + settings.set("GPU", "ScaledDithering", system.config.get("duckstation_dithering", "true")) + # Disable Interlacing + settings.set("GPU", "DisableInterlacing", system.config.get("duckstation_interlacing", "false")) + # Anti-Aliasing + if antialiasing := system.config.get("duckstation_antialiasing"): + if 'ssaa' in antialiasing: + settings.set("GPU", "PerSampleShading", "true") + parts = antialiasing.split('-') + multisamples = parts[0] + settings.set("GPU", "Multisamples", multisamples) + else: + settings.set("GPU", "Multisamples", antialiasing) + settings.set("GPU", "PerSampleShading", "false") + + ## [Display] + if not settings.has_section("Display"): + settings.add_section("Display") + # Aspect Ratio + aspect_ratio = system.config.get("duckstation_ratio") + settings.set("Display", "AspectRatio", aspect_ratio or "Auto (Game Native)") + + if aspect_ratio is not system.config.MISSING and aspect_ratio != "4:3": + system.config['bezel'] = "none" + # Vsync + settings.set("Display", "VSync", system.config.get("duckstation_vsync", "false")) + # CropMode + settings.set("Display", "CropMode", system.config.get("duckstation_CropMode", "Overscan")) + # Enable Frameskipping = option missing + settings.set("Display", "DisplayAllFrames", "false") + # OSD Messages + settings.set("Display", "ShowOSDMessages", system.config.get("duckstation_osd", "false")) + # Optimal frame pacing + settings.set("Display","DisplayAllFrames", system.config.get("duckstation_ofp", "false")) + # Integer Scaling + settings.set("Display","IntegerScaling", system.config.get("duckstation_integer", "false")) + # Linear Filtering + settings.set("Display","LinearFiltering", system.config.get("duckstation_linear", "false")) + # Stretch + stretch = system.config.get("duckstation_stretch", "false") + settings.set("Display","Stretch", stretch) + + if stretch == "true" and system.config.get("duckstation_integer", "false") == "false": + system.config['bezel'] = "none" + + ## [Audio] + if not settings.has_section("Audio"): + settings.add_section("Audio") + settings.set("Audio","StretchMode", system.config.get("duckstation_audio_mode", "TimeStretch")) + + ## [GameList] + if not settings.has_section("GameList"): + settings.add_section("GameList") + settings.set("GameList" , "RecursivePaths", "/userdata/roms/psx") + + ## [Cheevos] + if not settings.has_section("Cheevos"): + settings.add_section("Cheevos") + # RetroAchievements + if system.config.get_bool('retroachievements'): + username = system.config.get('retroachievements.username', "") + hardcore = system.config.get('retroachievements.hardcore', "") + presence = system.config.get('retroachievements.richpresence', "") + indicator = system.config.get('retroachievements.challenge_indicators', "") + leaderbd = system.config.get('retroachievements.leaderboards', "") + token = system.config.get('retroachievements.token', "") + unofficial = system.config.get('retroachievements.unofficial', "") + settings.set("Cheevos", "Enabled", "true") + settings.set("Cheevos", "Username", username) + settings.set("Cheevos", "Token", token) + if hardcore == '1': + settings.set("Cheevos", "ChallengeMode", "true") # For "hardcore" retroachievement points (no save, no rewind...) + else: + settings.set("Cheevos", "ChallengeMode", "false") + if presence == '1': + settings.set("Cheevos", "RichPresence", "true") # Enable rich presence information will be collected and sent to the server where supported + else: + settings.set("Cheevos", "RichPresence", "false") + if indicator == '1': + settings.set("Cheevos", "PrimedIndicators", "true") + else: + settings.set("Cheevos", "PrimedIndicators", "false") + if leaderbd == '1': + settings.set("Cheevos", "Leaderboards", "true") + else: + settings.set("Cheevos", "Leaderboards", "false") + if unofficial == '1': + settings.set("Cheevos", "UnofficialTestMode", "true") + else: + settings.set("Cheevos", "UnofficialTestMode", "false") + #settings.set("Cheevos", "UseFirstDiscFromPlaylist", "false") # When enabled, the first disc in a playlist will be used for achievements, regardless of which disc is active + #settings.set("Cheevos", "TestMode", "false") # DuckStation will assume all achievements are locked and not send any unlock notifications to the server. + else: + settings.set("Cheevos", "Enabled", "false") + + ## [TextureReplacements] + if not settings.has_section("TextureReplacements"): + settings.add_section("TextureReplacements") + # Texture Replacement saves\textures\psx game id - by default in Normal + enable_vram_write_replacements = "true" + preload_textures = "false" + + match system.config.get("duckstation_custom_textures"): + case "false" | "0": + enable_vram_write_replacements = "false" + case "preload": + preload_textures = "true" + + settings.set("TextureReplacements", "EnableVRAMWriteReplacements", enable_vram_write_replacements) + settings.set("TextureReplacements", "PreloadTextures", preload_textures) + + if not settings.has_section("InputSources"): + settings.add_section("InputSources") + settings.set("InputSources", "SDL", "true") + settings.set("InputSources", "SDLControllerEnhancedMode", "false") + settings.set("InputSources", "Evdev", "false") + settings.set("InputSources", "XInput", "false") + settings.set("InputSources", "RawInput", "false") + + ## [MemoryCards] + if not settings.has_section("MemoryCards"): + settings.add_section("MemoryCards") + # Set memory card location + settings.set("MemoryCards", "Directory", "../../../saves/duckstation/memcards") + + ## [Folders] + if not settings.has_section("Folders"): + settings.add_section("Folders") + # Set other folder locations too + mkdir_if_not_exists(CACHE / "duckstation") + settings.set("Folders", "Cache", "../../cache/duckstation") + mkdir_if_not_exists(SCREENSHOTS) + settings.set("Folders", "Screenshots", "../../../screenshots") + mkdir_if_not_exists(SAVES / "duckstation") + settings.set("Folders", "SaveStates", "../../../saves/duckstation") + mkdir_if_not_exists(CHEATS / "duckstation") + settings.set("Folders", "Cheats", "../../../cheats/duckstation") + + ## [Pad] + # Clear existing Pad(x) configs + for i in range(1, 9): + if settings.has_section(f"Pad{i}"): + settings.remove_section(f"Pad{i}") + # Now create Pad1 - 8 None to start + for i in range(1, 9): + settings.add_section(f"Pad{i}") + settings.set(f"Pad{i}", "Type", "None") + # Start with mutitap disabled + settings.set("ControllerPorts", "MultitapMode", "Disabled") + # Now add the controller config based on the ES type & number connected + for nplayer, pad in enumerate(playersControllers[:8], start=1): + # automatically add the multi-tap + if nplayer > 2: + settings.set("ControllerPorts", "MultitapMode", "Port1Only") + if nplayer > 4: + settings.set("ControllerPorts", "MultitapMode", "BothPorts") + pad_num = f"Pad{nplayer}" + sdl_num = f"SDL-{pad.index}" + ctrl_num = f"Controller{nplayer}" + # SDL2 configs are always the same for controllers + settings.set(pad_num, "Type", system.config.get(f"duckstation_{ctrl_num}", "DigitalController")) + settings.set(pad_num, "Up", f"{sdl_num}/DPadUp") + settings.set(pad_num, "Right", f"{sdl_num}/DPadRight") + settings.set(pad_num, "Down", f"{sdl_num}/DPadDown") + settings.set(pad_num, "Left", f"{sdl_num}/DPadLeft") + settings.set(pad_num, "Triangle", f"{sdl_num}/Y") + settings.set(pad_num, "Circle", f"{sdl_num}/B") + settings.set(pad_num, "Cross", f"{sdl_num}/A") + settings.set(pad_num, "Square", f"{sdl_num}/X") + settings.set(pad_num, "Select", f"{sdl_num}/Back") + settings.set(pad_num, "Start", f"{sdl_num}/Start") + settings.set(pad_num, "L1", f"{sdl_num}/LeftShoulder") + settings.set(pad_num, "R1", f"{sdl_num}/RightShoulder") + settings.set(pad_num, "L2", f"{sdl_num}/+LeftTrigger") + settings.set(pad_num, "R2", f"{sdl_num}/+RightTrigger") + settings.set(pad_num, "L3", f"{sdl_num}/LeftStick") + settings.set(pad_num, "R3", f"{sdl_num}/RightStick") + settings.set(pad_num, "LLeft", f"{sdl_num}/-LeftX") + settings.set(pad_num, "LRight", f"{sdl_num}/+LeftX") + settings.set(pad_num, "LDown", f"{sdl_num}/+LeftY") + settings.set(pad_num, "LUp", f"{sdl_num}/-LeftY") + settings.set(pad_num, "RLeft", f"{sdl_num}/-RightX") + settings.set(pad_num, "RRight", f"{sdl_num}/+RightX") + settings.set(pad_num, "RDown", f"{sdl_num}/+RightY") + settings.set(pad_num, "RUp", f"{sdl_num}/-RightY") + settings.set(pad_num, "SmallMotor", f"{sdl_num}/SmallMotor") + settings.set(pad_num, "LargeMotor", f"{sdl_num}/LargeMotor") + settings.set(pad_num, "VibrationBias", "8") + # D-Pad to Joystick + digitalmode = system.config.get("duckstation_digitalmode") + settings.set(pad_num, "AnalogDPadInDigitalMode", digitalmode or "false") + if digitalmode and system.config.get(f"duckstation_{ctrl_num}") == "AnalogController": + settings.set(pad_num, "Analog", f"{sdl_num}/Guide") + # NeGcon ? + if system.config.get(f"duckstation_{ctrl_num}") == "NeGcon": + settings.set(pad_num, "A", f"{sdl_num}/B") + settings.set(pad_num, "B", f"{sdl_num}/Y") + settings.set(pad_num, "I", f"{sdl_num}/+RightTrigger") + settings.set(pad_num, "II", f"{sdl_num}/+LeftTrigger") + settings.set(pad_num, "L", f"{sdl_num}/LeftShoulder") + settings.set(pad_num, "R", f"{sdl_num}/RightShoulder") + settings.set(pad_num, "SteeringLeft", f"{sdl_num}/-LeftX") + settings.set(pad_num, "SteeringRight", f"{sdl_num}/+LeftX") + # Mouse + if system.config.get(f"duckstation_{ctrl_num}") == "PlayStationMouse": + settings.set(pad_num, "Right", f"{sdl_num}/B") + settings.set(pad_num, "Left", f"{sdl_num}/A") + settings.set(pad_num, "RelativeMouseMode", f"{sdl_num}true") + + # Guns - configure based on detected guns, not controllers + if system.config.use_guns and guns: + for nplayer, _ in enumerate(guns[:8], start=1): + pad_num = f"Pad{nplayer}" + gun_num = f"Pointer-{nplayer - 1}" + # Gun mapping is hardcoded into patch. + # Justifier ROM mapping: BTN_LEFT = Trigger | BTN_RIGHT = Back | BTN_MIDDLE = Start + if metadata.get("gun_type") == "justifier": + settings.set(pad_num, "Type", "Justifier") + else: + # GunCon ROM mapping: BTN_LEFT = Trigger | BTN_RIGHT = A | BTN_MIDDLE = B + settings.set(pad_num, "Type", "GunCon") + # Pedal key for button A + pedalsKeys = {1: "c", 2: "v", 3: "b", 4: "n"} + pedalkey = system.config.get(f"controllers.pedals{nplayer}", pedalsKeys.get(nplayer)) + if pedalkey: + settings.set(pad_num, "A", f"{gun_num}/RightButton & Keyboard/{pedalkey.upper()}") + else: + settings.set(pad_num, "A", f"{gun_num}/RightButton") + # Crosshairs - BGR color code. Player 1 red; player 2 blue. + settings.set(pad_num, "CrosshairScale", system.config.get("duckstation_crosshair", "0")) + settings.set(pad_num, "CrosshairColor", "0000FF" if nplayer == 1 else "FF0000") + + ## [Hotkeys] + if not settings.has_section("Hotkeys"): + settings.add_section("Hotkeys") + # Force defaults to be aligned with evmapy + settings.set("Hotkeys", "FastForward", "Keyboard/Tab") + settings.set("Hotkeys", "Reset", "Keyboard/F6") + settings.set("Hotkeys", "LoadSelectedSaveState", "Keyboard/F1") + settings.set("Hotkeys", "SaveSelectedSaveState", "Keyboard/F2") + settings.set("Hotkeys", "SelectPreviousSaveStateSlot", "Keyboard/F3") + settings.set("Hotkeys", "SelectNextSaveStateSlot", "Keyboard/F4") + settings.set("Hotkeys", "Screenshot", "Keyboard/F10") + settings.set("Hotkeys", "Rewind", "Keyboard/F5") + settings.set("Hotkeys", "OpenPauseMenu", "Keyboard/F7") + settings.set("Hotkeys", "ChangeDisc", "Keyboard/F8") + if settings.has_option('Hotkeys', 'OpenQuickMenu'): + settings.remove_option('Hotkeys', 'OpenQuickMenu') + + ## [CDROM] + if not settings.has_section("CDROM"): + settings.add_section("CDROM") + + settings.set("CDROM", "AllowBootingWithoutSBIFile", system.config.get("duckstation_boot_without_sbi", "false")) + + ## [UI] + if not settings.has_section("UI"): + settings.add_section("UI") + settings.set("UI", "UnofficialBuildWarningConfirmed", "true") + + # Save config + with ensure_parents_and_open(settings_path, 'w') as configfile: + settings.write(configfile) + + # write our own gamecontrollerdb.txt file before launching the game + dbfile = "/usr/share/duckstation/resources/gamecontrollerdb.txt" + write_sdl_controller_db(playersControllers, dbfile) + + # 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 melonDS, see + # melonds.mk) + if environ.get("WAYLAND_DISPLAY"): + qt_qpa_platform = "wayland" + elif environ.get("DISPLAY"): + qt_qpa_platform = "xcb" + else: + qt_qpa_platform = "eglfs" + + # use their modified shaderc library + return Command.Command( + array=commandArray, + env={ + "LD_LIBRARY_PATH": "/usr/stenzek-shaderc/lib:/usr/lib", + "XDG_CONFIG_HOME": CONFIGS, + "QT_QPA_PLATFORM": qt_qpa_platform, + "SDL_GAMECONTROLLERCONFIG": generate_sdl_game_controller_config(playersControllers), + "SDL_JOYSTICK_HIDAPI": "0" + } + ) + +def getLangFromEnvironment(): + lang = environ['LANG'][:5] + availableLanguages = { "en_US": "en", + "de_DE": "de", + "fr_FR": "fr", + "es_ES": "es", + "he_IL": "he", + "it_IT": "it", + "ja_JP": "ja", + "nl_NL": "nl", + "pl_PL": "pl", + "pt_BR": "pt-br", + "pt_PT": "pt-pt", + "ru_RU": "ru", + "zh_CN": "zh-cn" + } + if lang in availableLanguages: + return availableLanguages[lang] + return availableLanguages["en_US"] + +def rewriteM3uFullPath(m3u: Path) -> Path: + # Rewrite a clean m3u file with valid fullpath + + # get initialm3u + with m3u.open() as f: + firstline = f.readline().rstrip() # Get first line in m3u + + initialfirstdisc = Path("/tmp") / Path(firstline).with_suffix(".m3u").name # Generating a temp path with the first iso filename in m3u + + # create a temp m3u to bypass Duckstation m3u bad pathfile + fulldirname = m3u.parent + with initialfirstdisc.open("w"): + pass + + with m3u.open() as initialm3u, initialfirstdisc.open('a') as f1: + for line in initialm3u: + # handle both "/MGScd1.chd" and "MGScd1.chd" + if line[0] == "/": + newpath = fulldirname / line[1:] + else: + newpath = fulldirname / line + f1.write(str(newpath)) + + return initialfirstdisc # Return the tempm3u pathfile written with valid fullpath + +def find_bios(bios_lists: Mapping[str, Sequence[str]]): + found_bios: dict[str, str] = {} + + try: + files_lower = {f.name.lower(): f.name for f in BIOS.iterdir()} + except OSError as e: + raise BatoceraException(f"Unable to read BIOS directory: {BIOS}") from e + + for region, bios_list in bios_lists.items(): + for bios in bios_list: + if bios.lower() in files_lower: + found_bios[region] = files_lower[bios.lower()] + break + + return found_bios + +# Define BIOS lists +bios_lists = { + "NTSCU": ["scph101.bin", "scph1001.bin", "scph5501.bin", "scph7001.bin", "scph7501.bin"], + "PAL": ["scph1002.bin", "scph5502.bin", "scph5552.bin", "scph7002.bin", "scph7502.bin", "scph9002.bin", "scph102a.bin", "scph102b.bin"], + "NTSCJ": ["scph100.bin", "scph1000.bin", "scph3000.bin", "scph3500.bin", "scph5500.bin", "scph7000.bin", "scph7003.bin"], + "Uni": ["psxonpsp660.bin", "ps1_rom.bin"] +} diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/flycast/flycastGenerator.py b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/flycast/flycastGenerator.py new file mode 100644 index 00000000000..d1659f73aa6 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/lib/python3.14/site-packages/configgen/generators/flycast/flycastGenerator.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +from os import environ +from shutil import copyfile +from typing import TYPE_CHECKING + +from batocera_common.configparser import CaseSensitiveConfigParser + +from ... import Command +from ...batoceraPaths import CONFIGS, ensure_parents_and_open, mkdir_if_not_exists +from ...controller import generate_sdl_game_controller_config +from ..Generator import Generator +from . import flycastControllers +from .flycastPaths import FLYCAST_BIOS, FLYCAST_CONFIG, FLYCAST_SAVES, FLYCAST_VMU_BLANK, FLYCAST_VMUA1, FLYCAST_VMUA2 + +if TYPE_CHECKING: + from ...types import HotkeysContext + + +class FlycastGenerator(Generator): + + def getHotkeysContext(self) -> HotkeysContext: + return { + "name": "flycast", + "keys": { + "exit": "KEY_F7", + "menu": "KEY_TAB", + "save_state": "KEY_F8", + "restore_state": "KEY_F9", + "fastforward": "KEY_SPACE" + } + } + + # Main entry of the module + # Configure fba and return a command + def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution): + # Write emu.cfg to map joysticks, init with the default emu.cfg + Config = CaseSensitiveConfigParser(interpolation=None) + if FLYCAST_CONFIG.exists(): + try: + Config.read(FLYCAST_CONFIG) + except Exception: + pass # give up the file + + if not Config.has_section("input"): + Config.add_section("input") + + # Lightguns - configure before controllers (guns take priority on ports) + gun_ports = set() + if system.config.use_guns and guns: + for nplayer, _ in enumerate(guns[:4], start=1): + Config.set("input", f'device{nplayer}', "7") # MDT_LightGun + Config.set("input", f'device{nplayer}.1', "1") # VMU + Config.set("input", f'device{nplayer}.2', "10") # None + gun_ports.add(nplayer) + + # For each pad detected + for controller in playersControllers: + # Skip ports already assigned to guns + if controller.player_number in gun_ports: + continue + + # Write the mapping files for Dreamcast + if (system.name == "dreamcast"): + flycastControllers.generateControllerConfig(controller, "dreamcast") + else: + # Write the Arcade variant (Atomiswave & Naomi/2) + flycastControllers.generateControllerConfig(controller, "arcade") + + # Set the controller type per Port + Config.set("input", f'device{controller.player_number}', "0") # Sega Controller + Config.set("input", f'device{controller.player_number}.1', "1") # Sega VMU + # Set controller pack, gui option + ctrlpackconfig = f"flycast_ctrl{controller.player_number}_pack" + Config.set("input", f'device{controller.player_number}.2', system.config.get_str(ctrlpackconfig, '1')) # Sega VMU + # Ensure controller(s) are on seperate Ports + port = controller.player_number-1 + Config.set("input", f'maple_sdl_joystick_{controller.index}', str(port)) + + # add the keyboard mappings for hotkeys + flycastControllers.generateKeyboardConfig() + + if not Config.has_section("config"): + Config.add_section("config") + if not Config.has_section("window"): + Config.add_section("window") + # ensure we are always fullscreen + Config.set("window", "fullscreen", "yes") + # set video resolution + Config.set("window", "width", str(gameResolution["width"])) + Config.set("window", "height", str(gameResolution["height"])) + # set render resolution - default 480 (Native) + Config.set("config", "rend.Resolution", system.config.get_str("flycast_render_resolution", "480")) + # wide screen mode - default off + Config.set("config", "rend.WideScreen", system.config.get_str("flycast_ratio", "no")) + # rotate option - default off + Config.set("config", "rend.Rotate90", system.config.get_str("flycast_rotate", "no")) + # renderer - default: OpenGL + renderer = system.config.get('flycast_renderer') + sorting = system.config.get('flycast_sorting') + + if renderer == "0": + if sorting == "3": + # per pixel + Config.set("config", "pvr.rend", "3") + else: + # per triangle + Config.set("config", "pvr.rend", "0") + elif renderer == "4": + if sorting == "3": + # per pixel + Config.set("config", "pvr.rend", "5") + else: + # per triangle + Config.set("config", "pvr.rend", "4") + else: + Config.set("config", "pvr.rend", "0") + if sorting == "3": + # per pixel + Config.set("config", "pvr.rend", "3") + + # anisotropic filtering + Config.set("config", "rend.AnisotropicFiltering", system.config.get_str("flycast_anisotropic", "1")) + # transparent sorting + # per strip + Config.set("config", "rend.PerStripSorting", "yes" if system.config.get("flycast_sorting") == "2" else "no") + + # [Dreamcast specifics] + # language + Config.set("config", "Dreamcast.Language", system.config.get_str("flycast_language", "1")) + # region + Config.set("config", "Dreamcast.Region", system.config.get_str("flycast_region", "1")) + # save / load states + Config.set("config", "Dreamcast.AutoLoadState", system.config.get_str("flycast_loadstate", "no")) + Config.set("config", "Dreamcast.AutoSaveState", system.config.get_str("flycast_savestate", "no")) + # windows CE + Config.set("config", "Dreamcast.ForceWindowsCE", system.config.get_str("flycast_winCE", "no")) + # Per-game VMU + Config.set("config", "PerGameVmu", system.config.get_bool("flycast_per_game_vmu", return_values=("yes", "no"))) + # DSP + Config.set("config", "aica.DSPEnabled", system.config.get_str("flycast_DSP", "no")) + # Guns crosshairs + Config.set("config", "rend.CrossHairColor1", system.config.get_str("flycast_lightgun1_crosshair", "0")) + Config.set("config", "rend.CrossHairColor2", system.config.get_str("flycast_lightgun2_crosshair", "0")) + Config.set("config", "rend.CrossHairColor3", system.config.get_str("flycast_lightgun3_crosshair", "0")) + Config.set("config", "rend.CrossHairColor4", system.config.get_str("flycast_lightgun4_crosshair", "0")) + + # Retroachievements + if not Config.has_section("achievements"): + Config.add_section("achievements") + + if system.config.get_bool('retroachievements'): + username = system.config.get('retroachievements.username', "") + hardcore = system.config.get_bool('retroachievements.hardcore', return_values=("yes", "no")) + token = system.config.get('retroachievements.token', "") + + # apply config + Config.set("achievements", "Enabled", "yes") + Config.set("achievements", "HardcoreMode", hardcore) + Config.set("achievements", "Token", token) + Config.set("achievements", "UserName" , username) + else: + Config.set("achievements", "Enabled", "no") + + # custom : allow the user to configure directly emu.cfg via batocera.conf via lines like : dreamcast.flycast.section.option=value + for section_option, user_config_value in system.config.items(starts_with='flycast.'): + custom_section, _, custom_option = section_option.partition('.') + if not Config.has_section(custom_section): + Config.add_section(custom_section) + Config.set(custom_section, custom_option, user_config_value) + + ### update the configuration file + with ensure_parents_and_open(FLYCAST_CONFIG, 'w+') as cfgfile: + Config.write(cfgfile) + cfgfile.close() + + # internal config + mkdir_if_not_exists(FLYCAST_SAVES) + + # vmuA1 + if not FLYCAST_VMUA1.is_file(): + copyfile(FLYCAST_VMU_BLANK, FLYCAST_VMUA1) + # vmuA2 + if not FLYCAST_VMUA2.is_file(): + copyfile(FLYCAST_VMU_BLANK, FLYCAST_VMUA2) + + # the command to run + commandArray = ['/usr/bin/flycast', rom] + + # SDL2 defaults to auto-detecting the video driver (wayland -> x11 -> ...); + # on boards with no compositor and no X11 (direct KMS/DRM only), that probe + # fails outright (XDG_RUNTIME_DIR missing, then "Can't load EGL/GL library + # on window creation") instead of falling back to kmsdrm, so force it. + if environ.get("WAYLAND_DISPLAY"): + sdl_videodriver = "wayland" + elif environ.get("DISPLAY"): + sdl_videodriver = "x11" + else: + sdl_videodriver = "kmsdrm" + + # Same reasoning for audio: this SDL2 is built with pulseaudio disabled + # in favor of pipewire (see sdl2.mk), and pipewire's client socket lives + # under XDG_RUNTIME_DIR, which emulatorlauncher doesn't set - without it + # SDL falls back to raw ALSA (fails: device already owned by pipewire) + # and even the pipewire driver itself can't find the socket. + sdl_audiodriver = "pipewire" + xdg_runtime_dir = environ.get("XDG_RUNTIME_DIR", "/run") + + # Here is the trick to make flycast find files : + # emu.cfg is in $XDG_CONFIG_DIRS or $XDG_CONFIG_HOME. + # VMU will be in $XDG_DATA_HOME / $FLYCAST_DATADIR because it needs rw access -> /userdata/saves/dreamcast + # $FLYCAST_BIOS_PATH is where Flaycast should find the bios files + # controller cfg files are set with an absolute path, so no worry + return Command.Command( + array=commandArray, + env={ + "XDG_CONFIG_HOME": CONFIGS, + "XDG_CONFIG_DIRS": CONFIGS, + "XDG_DATA_HOME": FLYCAST_SAVES.parent, + "FLYCAST_DATADIR": FLYCAST_SAVES.parent, + "FLYCAST_BIOS_PATH": FLYCAST_BIOS, + "SDL_GAMECONTROLLERCONFIG": generate_sdl_game_controller_config(playersControllers), + "SDL_JOYSTICK_HIDAPI": "0", + "SDL_VIDEODRIVER": sdl_videodriver, + "SDL_AUDIODRIVER": sdl_audiodriver, + "XDG_RUNTIME_DIR": xdg_runtime_dir + } + ) diff --git a/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/share/libretro/info/fbneo_korean_libretro.info b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/share/libretro/info/fbneo_korean_libretro.info new file mode 120000 index 00000000000..b8765442f75 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/fsoverlay/usr/share/libretro/info/fbneo_korean_libretro.info @@ -0,0 +1 @@ +fbneo_libretro.info \ No newline at end of file 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..555d81e1799 --- /dev/null +++ b/board/batocera/rockchip/bsp-odroidm1/local.mk.example @@ -0,0 +1,36 @@ +# 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 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. +# +# 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 diff --git a/board/batocera/scripts/post-image-script.sh b/board/batocera/scripts/post-image-script.sh index e750381ca6e..5eda1aece93 100755 --- a/board/batocera/scripts/post-image-script.sh +++ b/board/batocera/scripts/post-image-script.sh @@ -13,7 +13,13 @@ GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp" ################################ ##### find images to build ##### -BATOCERA_TARGET=$(grep -E "^BR2_PACKAGE_BATOCERA_TARGET_[A-Z_0-9]*=y$" "${BR2_CONFIG}" | grep -vE "_ANY=" | grep -vE "_GLES[0-9]*=" | sed -e s+'^BR2_PACKAGE_BATOCERA_TARGET_\([A-Z_0-9]*\)=y$'+'\1'+) +# _BSP_ excluded: BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 is a board-identity +# flag (see package/batocera/core/batocera-system/Config.in), not one of the +# real BATOCERA_TARGET_* board symbols - it's always set alongside +# BATOCERA_TARGET_RK3568 on this board, so without this exclusion the grep +# matches both lines and BATOCERA_TARGET becomes "RK3568\nBSP_ODROIDM1" +# (literal embedded newline), corrupting the final image filename. +BATOCERA_TARGET=$(grep -E "^BR2_PACKAGE_BATOCERA_TARGET_[A-Z_0-9]*=y$" "${BR2_CONFIG}" | grep -vE "_ANY=" | grep -vE "_GLES[0-9]*=" | grep -vE "_BSP_" | sed -e s+'^BR2_PACKAGE_BATOCERA_TARGET_\([A-Z_0-9]*\)=y$'+'\1'+) BATOCERA_LOWER_TARGET=$(echo "${BATOCERA_TARGET}" | tr '[:upper:]' '[:lower:]') BATOCERA_IMAGES_TARGETS=$(grep -E "^BR2_TARGET_BATOCERA_IMAGES[ ]*=[ ]*\".*\"[ ]*$" "${BR2_CONFIG}" | sed -e s+"^BR2_TARGET_BATOCERA_IMAGES[ ]*=[ ]*\"\(.*\)\"[ ]*$"+"\1"+) if test -z "${BATOCERA_IMAGES_TARGETS}" diff --git a/buildroot b/buildroot index 95d26b3e3ff..636116a5c1f 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit 95d26b3e3ff0a88446aaed4ab2a27c9504ae838c +Subproject commit 636116a5c1f765be6a991ad52cdd9316fc312fed diff --git a/configs/batocera-rk3568-odroidm1.board b/configs/batocera-rk3568-odroidm1.board index 1eff96f5e92..bbf44f855e9 100644 --- a/configs/batocera-rk3568-odroidm1.board +++ b/configs/batocera-rk3568-odroidm1.board @@ -89,7 +89,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 @@ -108,7 +114,7 @@ BR2_PACKAGE_BROTLI=y # CPU-bound even on TOPST D3-G's stronger Cortex-A72 (~18fps). BR2_PACKAGE_LIBRETRO_DOLPHIN=y -# FBNeo core built from dsno/Zansword's Korean-patched fork (CRC-tolerant +# FBNeo core built from DsNo's Korean-patched fork (CRC-tolerant # driver patches for Korean-translated arcade ROMs, which the standard # libretro-fbneo core rejects on CRC32 mismatch) - registered as an # alternate "fbneo_korean" core alongside the standard one, same fbneo @@ -116,6 +122,13 @@ BR2_PACKAGE_LIBRETRO_DOLPHIN=y # libretro-fbneo-korean/. BR2_PACKAGE_LIBRETRO_FBNEO_KOREAN=y +# batocera-system/Config.in's default select list for beetle-saturn only +# covers a fixed board whitelist (x86/rk3588/qcs6490/sm8xxx/...) that +# doesn't include our RK3568 family, so force it on directly - alternate +# Saturn core for comparison against yabasanshiro (mednafen-accuracy +# tradeoff, known slow on this CPU from earlier live testing). +BR2_PACKAGE_LIBRETRO_BEETLE_SATURN=y + # Saturn: tried enabling kronos here (upstream only selects it for x86, # but its .mk already supports several ARM+GLES boards via FORCE_GLES=1 - # added an RK3568 branch reusing the odroid-c4 GLES platform, see @@ -138,6 +151,103 @@ 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. +# +# 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 +# +# Side effect discovered later (2026-08-23, v15 build): this hard-selects +# BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_VULKAN (batocera-system/Config.in) - +# a `select`, so it can't be undone from a defconfig. That GST plugin needs +# a host shader compiler we don't have; see the actual fix (gated on a +# nonexistent BR2_PACKAGE_HOST_SHADERC) in buildroot/package/gstreamer1/ +# gst1-plugins-bad/gst1-plugins-bad.mk (our buildroot fork, not upstream). +# +# 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): +# +# 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 + +# AetherSX2 (PS2) - vendored prebuilt aarch64 binary (ROCKNIX), uses the +# older/pre-mainline GS renderer that survives on this blob (bmdhacks +# pcsx2's newer GS trips DATA_INVALID_FAULT under load - see project +# history for the cross-reproduction that pinned this down). This line +# was missing entirely (package existed in the tree but was never +# actually enabled here), so the ps2 system had no valid emulator and +# was silently absent from ES's system list. +BR2_PACKAGE_AETHERSX2=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-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. +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. + +# 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). + # Firmware BR2_PACKAGE_FIRMWARE_ARMBIAN=y diff --git a/docker/docker.mk b/docker/docker.mk index fa44d61c299..8658efaf49e 100644 --- a/docker/docker.mk +++ b/docker/docker.mk @@ -23,6 +23,37 @@ else UID := $(shell id -u) GID := $(shell id -g) +# Opt-in extra bind mount for iterative package development: point +# KERNEL_SRCDIR 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 + +# git worktree checkouts (as opposed to a plain clone) keep their real +# .git contents in the *main* checkout's .git/worktrees//, outside +# PROJECT_DIR entirely - the worktree's own ".git" is just a one-line +# pointer file with an absolute path to that location. Since only +# PROJECT_DIR gets bind-mounted above, `git` commands run inside the +# container (e.g. batocera-system.mk's BATOCERA_SYSTEM_COMMIT) can't +# resolve it and silently fail ("fatal: not a git repository"), leaving +# batocera.version's commit suffix empty. Mount the real common dir at +# the same absolute host path so it resolves identically in-container. +GIT_COMMON_DIR := $(shell cd $(PROJECT_DIR) && git rev-parse --git-common-dir 2>/dev/null | xargs -I{} realpath {} 2>/dev/null) +ifneq ($(GIT_COMMON_DIR),) +ifeq ($(filter $(PROJECT_DIR)/%,$(GIT_COMMON_DIR)),) +DOCKER_OPTS += -v $(GIT_COMMON_DIR):$(GIT_COMMON_DIR):ro +endif +endif + define RUN_DOCKER $(DOCKER) run -t --init --rm \ -v $(PROJECT_DIR):/build \ diff --git a/package/batocera/controllers/pads/xone/xone.mk b/package/batocera/controllers/pads/xone/xone.mk index a52f12b9b82..1bcb5b88bc8 100644 --- a/package/batocera/controllers/pads/xone/xone.mk +++ b/package/batocera/controllers/pads/xone/xone.mk @@ -5,7 +5,19 @@ ################################################################################ # Workaround the need for Kernel 5.11 or greater with some boards -ifeq ($(BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_5_4)$(BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_5_10)$(BR2_KERNEL_HEADERS_5_15),y) +# +# This board reports BR2_KERNEL_HEADERS_6_1 so it falls into the v0.5.8 +# branch below by this heuristic, but it's a heavily backported hardkernel +# BSP fork (Pyohwan/linux, fix-hdmi-dsi-dual-livelock), not a vanilla 6.1 - +# it already carries the newer `const struct device *` device_type.uevent +# signature that upstream mainline only picked up much later. v0.5.8's +# bus.c (and its crypto_akcipher_sync_encrypt() call) predates that, so it +# fails to build here with -Wincompatible-pointer-types / +# implicit-function-declaration errors. The bbf0dcc commit below is newer +# (unreleased, post-v0.5.8) xone main-branch code that already handles +# this - confirmed building clean against this exact kernel +# (batocera.linux output-malig29p1, 2026-08-15). +ifeq ($(BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_5_4)$(BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_5_10)$(BR2_KERNEL_HEADERS_5_15)$(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),y) XONE_VERSION = bbf0dcc484c3f5611f4e375da43e0e0ef08f3d18 else XONE_VERSION = v0.5.8 diff --git a/package/batocera/core/batocera-audio/S27audioconfig b/package/batocera/core/batocera-audio/S27audioconfig index 27cf7a09032..6be48307910 100755 --- a/package/batocera/core/batocera-audio/S27audioconfig +++ b/package/batocera/core/batocera-audio/S27audioconfig @@ -38,16 +38,7 @@ apply_audioconfig() { fi # Set Default Sink - # "auto" is applied too (not just explicit device names): on boards - # where batocera-audio's "set auto" resolves to a board-specific - # default (e.g. rk3568/odroidm1 following the active display), skipping - # it here left the sink wherever PipeWire's own policy happened to put - # it, never actually applying that default at boot. - if [ "${DEVICE}" = "auto" ]; then - log "info" "Applying device: auto" - ERR=$(batocera-audio set auto 2>&1) - [ $? -ne 0 ] && log "err" "Set-device (auto) failed: $ERR" && EXITCODE=1 - elif test -n "${DEVICE}"; then + if test -n "${DEVICE}" && [ "${DEVICE}" != "auto" ]; then log "info" "Applying device: ${DEVICE}" # Check if the device is actually in the list we just found if [[ ! "$CURRENT_SINKS" =~ "$DEVICE" ]]; then diff --git a/package/batocera/core/batocera-audio/alsa/batocera-audio b/package/batocera/core/batocera-audio/alsa/batocera-audio index 09d03d2d4cc..16a574581c9 100644 --- a/package/batocera/core/batocera-audio/alsa/batocera-audio +++ b/package/batocera/core/batocera-audio/alsa/batocera-audio @@ -65,41 +65,6 @@ case "${ACTION}" in LC_ALL=C pactl set-default-sink INTERNAL amixer -c 0 cset name='Playback Path' SPK ;; - "rk3568") - # "rk3568" is the shared arch for every board in - # this family (rock-3a, several anbernic/orangepi - # boards, ...), not just ours - the rk809 codec - # and the HDMI sink's sysfs-path-based name below - # are specific to the ODROID-M1's hardware/DTB - # and don't hold for the others (no rk809 sound - # card there, HDMI sink name likely differs). - # Scope to our board via batocera.board (same - # mechanism the "ogu" check above uses) so this - # doesn't break `set auto` on every other rk3568 - # board. - if [[ "$BOARD" == "bsp-odroidm1" ]]; then - # Follow HDMI's own DRM hotplug status rather - # than whether the DSI/VU8M overlay happened - # to be baked into this boot's DTB (config.ini's - # overlays= - see boot.cmd). DSI panels have no - # hotplug detection, so "DSI-1 present in DRM" - # only ever reflected a build-time choice, not - # whether a cable is actually plugged in - a - # boot with no VU8M overlay and no HDMI cable - # connected would still get routed to HDMI and - # produce no sound. The internal speaker needs - # no such check: it's a fixed onboard - # component, always present whenever the board - # is powered, so it's the correct default - # whenever HDMI isn't actively connected. - if modetest -c 2>/dev/null | awk -F'\t' '$4 ~ /^HDMI-A-1/ {print $3}' | grep -qx connected; then - MODE=alsa_output._sys_devices_platform_hdmi-sound_sound_card1.stereo-fallback - else - MODE=alsa_output._sys_devices_platform_rk809-sound_sound_card0.stereo-fallback - fi - LC_ALL=C pactl set-default-sink "${MODE}" || exit 1 - fi - ;; *) # do nothing! ;; diff --git a/package/batocera/core/batocera-audio/soundconfig b/package/batocera/core/batocera-audio/soundconfig index 501ec115509..2e56a76f9a3 100644 --- a/package/batocera/core/batocera-audio/soundconfig +++ b/package/batocera/core/batocera-audio/soundconfig @@ -154,22 +154,6 @@ mixer $card 'HDMI CTRL SRC' 'I2S' # Odroid Go Advance Speaker mixer $card 'Playback Path' 'SPK' -# ODROID-M1 rk817-codec (RK809 PMIC audio): the ALSA control reports -# "Limits: Playback 0 - 255" but the kernel driver only actually accepts -# up to 252 (confirmed via dmesg: "rk817_dac_vol_put: Volume out of range -# [0, 252]"). The generic "mixer $card DAC 100%" above silently fails on -# this board since 100% maps to the (invalid) top of the reported range, -# leaving playback stuck at whatever the driver's power-on default is -# (~80%, -20dB) regardless of what PipeWire's own volume shows. Called -# directly (not via the mixer() helper) since DAC is volume-only - the -# helper's default trailing "on" switch arg makes amixer reject the whole -# command, confirmed live with `bash -x`. -case ${BOARD} in - *odroidm1*) - amixer -c $card sset DAC 252,252 >/dev/null 2>&1 - ;; -esac - # ES8316 headphone jack mixer $card 'Right Headphone Mixer Right DAC' on mixer $card 'Left Headphone Mixer Left DAC' on diff --git a/package/batocera/core/batocera-configgen/batocera-configgen.mk b/package/batocera/core/batocera-configgen/batocera-configgen.mk index fa77f3a8c2c..b406ffdcaf0 100644 --- a/package/batocera/core/batocera-configgen/batocera-configgen.mk +++ b/package/batocera/core/batocera-configgen/batocera-configgen.mk @@ -57,6 +57,12 @@ else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_A3GEN2),y) BATOCERA_CONFIGGEN_SYSTEM=a3gen2 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3328),y) BATOCERA_CONFIGGEN_SYSTEM=rk3328 +else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),y) + # Own defaults file, not the shared rk3568 one below - the disabled + # hud_support entries there are Mali-blob-specific (mangohud's EGL + # hook crashes on it), not applicable to the other rk3568-family + # boards (mainline kernel + Panfrost). + BATOCERA_CONFIGGEN_SYSTEM=rk3568-odroidm1 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3568),y) BATOCERA_CONFIGGEN_SYSTEM=rk3568 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3326),y) diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py index 17562319ec2..d163913c69a 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/emulatorlauncher.py @@ -271,6 +271,19 @@ def start_rom(args: argparse.Namespace, maxnbplayers: int, rom: Path, original_r except Exception as overlay_error: _logger.error("Could not initialize standalone bezel overlay: %s", overlay_error) + # Mesa's own on-screen graph (fps/frametime) via the gallium + # driver, for boards where mangohud can't be used (its EGL + # hook crashes on some vendor blobs) - see the + # gallium_hud_fallback default in each board's + # configgen-defaults-.yml. Opt-in, not unconditional: + # on a genuinely Mesa/gallium board this draws a second + # overlay on top of mangohud or the generator's own fps + # counter (e.g. retroarch's fps_show, dolphin's ShowFPS), + # so it's only meant as a fallback for boards that can't + # use those. + if system.config.show_fps and system.config.get_bool('gallium_hud_fallback'): + cmd.env["GALLIUM_HUD"] = "fps" + # generate the gun help try: default_gun_help_dir = Path("/var/run/batocera-overlays") 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..2224770792b --- /dev/null +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py @@ -0,0 +1,168 @@ +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 ...controller import Controllers + from ...Emulator import Emulator + from ...types import HotkeysContext + +_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, playersControllers) + 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, playersControllers: Controllers) -> 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") + # Without this, AetherSX2's own settings-version check treats the ini as + # missing/wrong-version and shows a "Settings failed to load... reset to + # defaults?" prompt on every launch - same key pcsx2Generator.py already + # sets for the mainline pcsx2-qt build, just missing here. + cfg.set("UI", "SettingsVersion", "1") + 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") + # Default (Auto=-1) tries OpenGL first on this fork, which fails outright + # in our headless/Wayland-only environment (no real GLX acceleration) - + # default to 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). Software (13) is left available as a + # user-selectable fallback since it doesn't need a GPU device at all. + cfg.set("EmuCore/GS", "Renderer", system.config.get("aethersx2_gfxbackend", "14")) + cfg.set("EmuCore/GS", "upscale_multiplier", system.config.get("aethersx2_resolution", "1")) + cfg.set("EmuCore/GS", "OsdShowFPS", "true" if system.config.show_fps else "false") + + ## [InputSources] + if not cfg.has_section("InputSources"): + cfg.add_section("InputSources") + cfg.set("InputSources", "Keyboard", "true") + cfg.set("InputSources", "Mouse", "true") + cfg.set("InputSources", "SDL", "true") + + ## [Pad1]/[Pad2] - the binary's own bundled default ini maps every pad + # input to a keyboard key (confirmed live: buttons did nothing on a real + # gamepad, only the keyboard worked, until this was added) - map to the + # actual connected SDL controllers instead, same scheme pcsx2Generator + # already uses successfully on this board for the sibling pcsx2 package. + for section_name in ("Pad1", "Pad2", "Pad3", "Pad4"): + if cfg.has_section(section_name): + cfg.remove_section(section_name) + + for nplayer, pad in enumerate(playersControllers, start=1): + if nplayer > 2: + break + pad_num = f"Pad{nplayer}" + sdl_num = f"SDL-{pad.index}" + cfg.add_section(pad_num) + cfg.set(pad_num, "Type", "DualShock2") + cfg.set(pad_num, "InvertL", "0") + cfg.set(pad_num, "InvertR", "0") + cfg.set(pad_num, "Deadzone", "0") + cfg.set(pad_num, "AxisScale", "1.33") + cfg.set(pad_num, "TriggerDeadzone", "0") + cfg.set(pad_num, "TriggerScale", "1") + cfg.set(pad_num, "LargeMotorScale", "1") + cfg.set(pad_num, "SmallMotorScale", "1") + cfg.set(pad_num, "ButtonDeadzone", "0") + cfg.set(pad_num, "PressureModifier", "0.5") + cfg.set(pad_num, "Up", f"{sdl_num}/DPadUp") + cfg.set(pad_num, "Right", f"{sdl_num}/DPadRight") + cfg.set(pad_num, "Down", f"{sdl_num}/DPadDown") + cfg.set(pad_num, "Left", f"{sdl_num}/DPadLeft") + # This fork's SDLInputSource predates PCSX2's later switch to + # position-based "FaceNorth/FaceEast/FaceSouth/FaceWest" binding + # names (what the sibling pcsx2 package uses) - it still expects the + # older Nintendo-layout "A/B/X/Y" semantic names. Confirmed live: the + # newer names silently bind to nothing (D-pad/sticks worked fine + # since those names didn't change, only face buttons did no-input). + # Values taken verbatim from ROCKNIX's own real-hardware-validated + # RK3566 default ini (same aethersx2-sa binary, same input source). + cfg.set(pad_num, "Triangle", f"{sdl_num}/X") + cfg.set(pad_num, "Circle", f"{sdl_num}/A") + cfg.set(pad_num, "Cross", f"{sdl_num}/B") + cfg.set(pad_num, "Square", f"{sdl_num}/Y") + cfg.set(pad_num, "Select", f"{sdl_num}/Back") + cfg.set(pad_num, "Start", f"{sdl_num}/Start") + cfg.set(pad_num, "L1", f"{sdl_num}/LeftShoulder") + cfg.set(pad_num, "L2", f"{sdl_num}/+LeftTrigger") + cfg.set(pad_num, "R1", f"{sdl_num}/RightShoulder") + cfg.set(pad_num, "R2", f"{sdl_num}/+RightTrigger") + cfg.set(pad_num, "L3", f"{sdl_num}/LeftStick") + cfg.set(pad_num, "R3", f"{sdl_num}/RightStick") + cfg.set(pad_num, "LUp", f"{sdl_num}/-LeftY") + cfg.set(pad_num, "LRight", f"{sdl_num}/+LeftX") + cfg.set(pad_num, "LDown", f"{sdl_num}/+LeftY") + cfg.set(pad_num, "LLeft", f"{sdl_num}/-LeftX") + cfg.set(pad_num, "RUp", f"{sdl_num}/-RightY") + cfg.set(pad_num, "RRight", f"{sdl_num}/+RightX") + cfg.set(pad_num, "RDown", f"{sdl_num}/+RightY") + cfg.set(pad_num, "RLeft", f"{sdl_num}/-RightX") + cfg.set(pad_num, "Analog", f"{sdl_num}/Guide") + cfg.set(pad_num, "LargeMotor", f"{sdl_num}/LargeMotor") + cfg.set(pad_num, "SmallMotor", f"{sdl_num}/SmallMotor") + + with ini_file.open("w") as f: + cfg.write(f) 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..c1d11c4bfa4 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 @@ -125,15 +125,15 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game # Enable MMU dolphinSettings.set("Core", "MMU", str(system.config.get_bool("enable_mmu"))) - # Backend - Default OpenGL - if system.config.get("gfxbackend") == "Vulkan": + # Backend - Default Vulkan (falls back to OpenGL if explicitly chosen or Vulkan unavailable) + if system.config.get("gfxbackend") == "OGL": + dolphinSettings.set("Core", "GFXBackend", "OGL") + else: dolphinSettings.set("Core", "GFXBackend", "Vulkan") # Check Vulkan if not vulkan.is_available(): _logger.debug("Vulkan driver is not available on the system. Using OpenGL instead.") dolphinSettings.set("Core", "GFXBackend", "OGL") - else: - dolphinSettings.set("Core", "GFXBackend", "OGL") # Wiimote scanning dolphinSettings.set("Core", "WiimoteContinuousScanning", "True") @@ -423,18 +423,36 @@ 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'): 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 } ) 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..afd4a4f3265 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,22 @@ 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. + # + # Nested inside the hotkey check (not a second top-level if): if a + # pad has no hotkey button mapped, RetroArch treats a lone + # input_exit_emulator_btn as unmodified - Start alone would quit + # mid-game on every such pad, on every board, the moment + # controllers are configured at all. + if '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( diff --git a/package/batocera/core/batocera-configgen/configgen/configgen/generators/melonds/melondsGenerator.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/melonds/melondsGenerator.py index c62e7bc7576..54fdb1cceb6 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/melonds/melondsGenerator.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/melonds/melondsGenerator.py @@ -241,10 +241,11 @@ def generate(self, system, rom, playersControllers, metadata, guns, wheels, game "XDG_CONFIG_HOME": CONFIGS, "XDG_DATA_HOME": SAVES, # Default Qt platform ("xcb") aborts immediately - this board - # has neither X11 nor a working Wayland-client EGL path (see - # melonds.mk). "eglfs" (direct KMS/GBM) is the one that - # actually works here, same EGL platform every other - # consumer on this board already uses. - "QT_QPA_PLATFORM": "eglfs" + # has no X11. "eglfs" was used historically (see melonds.mk) + # while Wayland-client EGL was broken, but Track C's labwc + # compositor now supports it - "wayland" is required for the + # melonds_dual_screen (Window0/Window1 -> separate outputs) + # feature, since eglfs has no per-window output placement. + "QT_QPA_PLATFORM": "wayland" } ) 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/configgen/configgen/generators/ppsspp/ppssppConfig.py b/package/batocera/core/batocera-configgen/configgen/configgen/generators/ppsspp/ppssppConfig.py index d17dc677dac..7a868f09b85 100644 --- a/package/batocera/core/batocera-configgen/configgen/configgen/generators/ppsspp/ppssppConfig.py +++ b/package/batocera/core/batocera-configgen/configgen/configgen/generators/ppsspp/ppssppConfig.py @@ -45,7 +45,7 @@ def createPPSSPPConfig(iniConfig: CaseSensitiveConfigParser, system: Emulator): iniConfig.add_section("Graphics") # Graphics Backend - gfxbackend = system.config.get("gfxbackend", "0 (OPENGL)") + gfxbackend = system.config.get("gfxbackend", "3 (VULKAN)") iniConfig.set("Graphics", "GraphicsBackend", gfxbackend) # If Vulkan if gfxbackend == "3 (VULKAN)": diff --git a/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568-odroidm1.yml b/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568-odroidm1.yml index 1f2d7dfe616..40521a028fb 100644 --- a/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568-odroidm1.yml +++ b/package/batocera/core/batocera-configgen/configs/configgen-defaults-rk3568-odroidm1.yml @@ -9,21 +9,28 @@ default: # boards get nothing unless they opt in too). On genuinely Mesa/gallium # boards this would draw a second overlay on top of mangohud or the # emulator's own fps counter, so it's opt-in rather than unconditional. -# gamecube/nds/wii/fmtowns/psx/ps2/flash/model3/model2 below are copied +# gamecube/nds/wii/fmtowns/psx/flash/model3/model2 below are copied # verbatim from the shared configgen-defaults-rk3568.yml - only one # arch-defaults file gets loaded per board, so there's no way to share # these without duplicating them. Kept manually in sync (same convention # as board/batocera/rockchip/bsp-odroidm1/patches/ffmpeg/patches-source.txt); # if the shared file's entries for these systems ever change, this file # needs the same edit or it silently falls behind. +# gamecube/wii diverge from the shared file on purpose: standalone dolphin +# (emulator: dolphin) is what's actually benchmarked/recommended on this +# board (libretro-dolphin's OpenGL path is the one that breaks under +# Wayland - see project_odroid_retro.md), not the shared file's +# libretro-dolphin default. dolphinGenerator.py already defaults to +# Vulkan unless gfxbackend=OGL is explicitly set, so no extra option +# needed here for that part. gamecube: - emulator: libretro + emulator: dolphin core: dolphin nds: emulator: drastic core: drastic wii: - emulator: libretro + emulator: dolphin core: dolphin fmtowns: emulator: libretro @@ -32,6 +39,18 @@ psx: options: hud_support: false # duckstation doesn't like it on n2+ n64: + emulator: mupen64plus + core: rice # standalone mupen64plus has no Vulkan renderer on this + # GPU class (GLES+Vulkan only, no desktop GL) - rice is the + # best-performing plugin benchmarked here (glide64mk2/gliden64 both + # slower). The shared configgen-defaults.yml default (glide64mk2) was + # never actually overridden by the "auto order" fix mentioned in + # project_odroid_retro.md - that fix reordered es_systems.yml's core + # listing, which has no effect on which core ES marks as "auto": that's + # driven by comparing against this emulator/core pair (see + # python-src/batocera-es-system's registry.py get_systems_metadata), + # not list order. Confirmed live: without this, auto silently ran + # glide64mk2 the whole time. options: hud_support: false # mupen64plus standalone + mangohud's EGL hooking: # the Mali blob doesn't implement the EGL extension function(s) @@ -45,7 +64,16 @@ n64: # since other rk3568-family boards (mainline kernel + Panfrost) don't # have this vendor-blob-specific mangohud EGL hook failure. dreamcast: + emulator: flycast + core: flycast # standalone flycast, not the shared file's + # libretro-flycast default - matches the benchmarked recommendation + # (all 4 combinations measured near-identical perf, but standalone + # avoids the extra libretro indirection). flycastGenerator.py's own + # renderer default is OpenGL (unlike dolphin/ppsspp, which both + # default to Vulkan already) - flycast_renderer=4 forces Vulkan + # (pvr.rend 4/5 in flycast's own config, see flycastGenerator.py). options: + flycast_renderer: "4" hud_support: false # same mangohud+Mali blob EGL crash as n64 above, # confirmed on standalone flycast (2026-08-07): SDL_CreateWindow fails # ("Can't load EGL/GL library on window creation") when mangohud wraps it. @@ -58,17 +86,25 @@ atomiswave: systemsp: options: hud_support: false # flycast core, same issue as dreamcast above +# ps2 is the one entry that genuinely differs from the shared file (not +# just a copy): the shared file's libretro-play default and pcsx2 both +# hit a Mali blob GPU fault (DATA_INVALID_FAULT, confirmed live via +# dmesg - "GPU fault 0x5b from job slot 0" - the game visibly hangs) that +# AetherSX2 avoids (older GS command pattern doesn't trigger it) - see +# project_odroid_retro.md's "Mali G52 blob 구조적 한계" section. pcsx2 is +# kept installed/selectable in ES as a fallback for games/scenarios where +# it happens not to fault, but must never be the auto default - it was, +# until this fix (2026-08-28, caught live: auto silently launched pcsx2, +# which promptly hung on a real GPU fault). ps2: - emulator: libretro - core: play + emulator: aethersx2 + core: aethersx2 flash: emulator: lightspark core: lightspark model3: emulator: supermodel - core: supermodel-legacy - options: - videomode: max-1920x1080 + core: supermodel model2: emulator: libretro core: mame diff --git a/package/batocera/core/batocera-scripts/scripts/batocera-es-swissknife b/package/batocera/core/batocera-scripts/scripts/batocera-es-swissknife index e59b95dc504..750cd86716a 100644 --- a/package/batocera/core/batocera-scripts/scripts/batocera-es-swissknife +++ b/package/batocera/core/batocera-scripts/scripts/batocera-es-swissknife @@ -117,7 +117,9 @@ case ${1,,} in test "${BOARD}" = "rpi4" && BOARD=rpi464 # "temporarily" download on rpi464 for rpi4 if [[ "${1,,}" == "--update" && $ret -eq 0 ]]; then - URL="https://updates.batocera.org/$BOARD/$BRANCH/last/batocera.version" + UPDATEURL="$(/usr/bin/batocera-settings-get updates.url)" + [[ -z "${UPDATEURL}" ]] && UPDATEURL="https://updates.batocera.org" + URL="${UPDATEURL}/$BOARD/$BRANCH/last/batocera.version" NET_VER=$(wget -q -O - "$URL") [[ $? -eq 0 ]] || NET_VER="Connection failed!" echo "Installed: $VER" diff --git a/package/batocera/core/batocera-scripts/scripts/batocera-power-mode b/package/batocera/core/batocera-scripts/scripts/batocera-power-mode index 5535735c8e1..06bbb6b0d93 100755 --- a/package/batocera/core/batocera-scripts/scripts/batocera-power-mode +++ b/package/batocera/core/batocera-scripts/scripts/batocera-power-mode @@ -40,29 +40,6 @@ set_governor() { done } -# Set GPU devfreq governor, if one exists (name-matched, not hardcoded to a -# specific SoC's devfreq node - e.g. Rockchip's is "fde60000.gpu", but this -# also covers other boards whose GPU devfreq node happens to be named with -# "gpu" in it). Falls back silently (no-op) if the requested governor isn't -# in that node's available list, or there's no GPU devfreq node at all - a -# board without devfreq or with a differently-named node just isn't affected. -set_gpu_governor() { - local GOVERNOR_NAME=$1 - - for devfreq in /sys/class/devfreq/*gpu*; do - [ -e "$devfreq/governor" ] || continue - local AVAILABLE - AVAILABLE=$(cat "$devfreq/available_governors" 2>/dev/null) - if [[ " $AVAILABLE " =~ [[:space:]]${GOVERNOR_NAME}[[:space:]] ]]; then - local CURRENT - CURRENT=$(cat "$devfreq/governor" 2>/dev/null) - if [ "$CURRENT" != "$GOVERNOR_NAME" ]; then - echo "$GOVERNOR_NAME" > "$devfreq/governor" 2>/dev/null - fi - fi - done -} - # Check if requested EPP is available check_epp() { local PREFERENCE=$1 @@ -91,8 +68,6 @@ set_epp() { # Set system default set_default() { SYSTEM_GOVERNOR="$(/usr/bin/batocera-settings-get-master system.cpu.governor)" - SYSTEM_GPU_GOVERNOR="$(/usr/bin/batocera-settings-get-master system.gpu.governor)" - set_gpu_governor "${SYSTEM_GPU_GOVERNOR:-simple_ondemand}" if check_governor "$SYSTEM_GOVERNOR"; then set_governor "$SYSTEM_GOVERNOR" @@ -114,7 +89,6 @@ handle_powermode() { case "$POWERMODE_NAME" in "highperformance") set_governor "performance" - set_gpu_governor "performance" if [ "$EPP_AVAILABLE" -eq 1 ]; then if check_epp "performance"; then set_epp "performance" @@ -132,7 +106,6 @@ handle_powermode() { elif check_governor "powersave"; then set_governor "powersave" fi - set_gpu_governor "simple_ondemand" # Handle EPP if [ "$EPP_AVAILABLE" -eq 1 ]; then @@ -145,7 +118,6 @@ handle_powermode() { ;; "powersaver") set_governor "powersave" - set_gpu_governor "powersave" if [ "$EPP_AVAILABLE" -eq 1 ]; then if check_epp "power"; then set_epp "power" diff --git a/package/batocera/core/batocera-scripts/scripts/batocera-storage-manager b/package/batocera/core/batocera-scripts/scripts/batocera-storage-manager index e76fab0f2dd..3e133d9f8b4 100644 --- a/package/batocera/core/batocera-scripts/scripts/batocera-storage-manager +++ b/package/batocera/core/batocera-scripts/scripts/batocera-storage-manager @@ -30,6 +30,15 @@ MERGERFS_OPTS="-o cache.files=partial,dropcacheonclose=true,category.create=eplf # --- HELPERS --- log_msg() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; } +# mergerfs is FUSE-based and needs /dev/fuse, but nothing before this script +# in the boot sequence guarantees the fuse module is loaded (unlike +# batocera-mount, which does this same modprobe before its own FUSE mounts). +# Without it, every mergerfs call below fails silently with "Live mount +# failed" and the ROM pool never gets populated - confirmed live on hardware. +ensure_fuse_loaded() { + [ -e /dev/fuse ] || modprobe fuse 2>>"$LOG_FILE" +} + log_initial_info() { local INIT_LOCK_DIR="/tmp/batocera-storage.init.lock" @@ -622,9 +631,17 @@ case "$COMMAND" in fi # Check Pending Flag - PENDING_FLAG="/tmp/storage.pending.$PARENT_DISK" + # Scoped per-partition (not per-disk): a disk with several partitions + # (e.g. leftover installs from a previous OS alongside a real ROMs + # partition) must let each partition get its own merge/format prompt + # independently. A disk-wide flag here means whichever partition udev + # happens to enumerate first claims it and every other partition on + # that disk is silently skipped ("Pending flag exists") for the rest + # of the boot - confirmed live on hardware with a 4-partition NVMe + # drive, where the real ROMs partition kept losing this race. + PENDING_FLAG="/tmp/storage.pending.$DEVNAME" if [ -f "$PENDING_FLAG" ]; then - log_msg "SKIP: [$DEVNAME] Pending flag exists for '$PARENT_DISK'." + log_msg "SKIP: [$DEVNAME] Pending flag exists." exit 0 fi @@ -737,7 +754,8 @@ case "$COMMAND" in FINAL_OPTS="${MERGERFS_OPTS},nonempty" log_msg "REFRESH: Executing mergerfs with: $VALID_BRANCHES" - + + ensure_fuse_loaded /usr/bin/mergerfs $FINAL_OPTS "$VALID_BRANCHES" "$POOL_PATH" 2>> "$LOG_FILE" if [ $? -eq 0 ]; then @@ -759,8 +777,8 @@ case "$COMMAND" in DEVICE="/dev/$DEVNAME" if [[ "$DEVNAME" =~ ^sd[a-z][0-9]*$ ]]; then PARENT_DISK="${DEVNAME%%[0-9]*}"; else PARENT_DISK=$(lsblk -no pkname "$DEVICE" 2>/dev/null); [ -z "$PARENT_DISK" ] && PARENT_DISK="$DEVNAME"; fi - - rm -f "/tmp/storage.pending.$PARENT_DISK" + + rm -f "/tmp/storage.pending.$DEVNAME" PARENT_LOCK="/tmp/storage.runtime.disk.$PARENT_DISK" mkdir "$PARENT_LOCK" 2>/dev/null || true trap "rmdir '$PARENT_LOCK' 2>/dev/null" EXIT @@ -811,7 +829,7 @@ case "$COMMAND" in sleep 2 # Manually Mount - mount_partition "$NEW_PART" "false" "/tmp/storage.pending.$PARENT_DISK" + mount_partition "$NEW_PART" "false" "/tmp/storage.pending.$(basename "$NEW_PART")" RET=$? if [ $RET -ne 0 ] && [ $RET -ne 2 ]; then @@ -853,8 +871,8 @@ case "$COMMAND" in PARENT_DISK=$(lsblk -no pkname "$DEV_SOURCE" 2>/dev/null) [ -z "$PARENT_DISK" ] && PARENT_DISK="$DEV_NAME" fi - - rm -f "/tmp/storage.pending.$PARENT_DISK" + + rm -f "/tmp/storage.pending.$DEV_NAME" if ! is_mounted "$MOUNT_POINT"; then REAL_PATH=$(readlink -f "$MOUNT_POINT") @@ -909,6 +927,7 @@ case "$COMMAND" in BRANCHES_CMD="$BASE_DIR:$FULL_BRANCHES" log_msg "MERGE: Executing mergerfs..." + ensure_fuse_loaded /usr/bin/mergerfs $MERGERFS_OPTS "$BRANCHES_CMD" "$POOL_PATH" if [ $? -eq 0 ]; then @@ -932,8 +951,8 @@ case "$COMMAND" in MOUNT_POINT=$(grep "^/dev/$DEVNAME " /proc/mounts | awk '{print $2}') if [ -z "$MOUNT_POINT" ]; then - # If not mounted, just clear the pending flag for the parent and exit - rm -f "/tmp/storage.pending.$PARENT_DISK" + # If not mounted, just clear this partition's pending flag and exit + rm -f "/tmp/storage.pending.$DEVNAME" exit 0 fi @@ -997,7 +1016,8 @@ case "$COMMAND" in send_notification "NOTIFY:Rebuilding Pool..." mkdir -p "$BASE_DIR" - BRANCHES_CMD="$BASE_DIR:$CLEAN_CONFIG" + BRANCHES_CMD="$BASE_DIR:$CLEAN_CONFIG" + ensure_fuse_loaded /usr/bin/mergerfs $MERGERFS_OPTS "$BRANCHES_CMD" "$POOL_PATH" if [ $? -eq 0 ]; then @@ -1090,13 +1110,14 @@ case "$COMMAND" in fi if [ "$MATCH" = true ]; then rm -f "/tmp/storage.pending.$D" - $0 detect "$D" & + $0 detect "$D" & # Trigger detection for every partition on this disk # This ensures handle_partition_event runs and mounts the volumes for part in /sys/class/block/$D/$D*; do [ -e "$part" ] || continue P=$(basename "$part") [ "$P" == "$D" ] && continue + rm -f "/tmp/storage.pending.$P" log_msg "IGNORE: Re-triggering partition detection for $P" $0 detect "$P" & done diff --git a/package/batocera/core/batocera-scripts/scripts/batocera-upgrade b/package/batocera/core/batocera-scripts/scripts/batocera-upgrade index 76240d5fb13..82c29574305 100755 --- a/package/batocera/core/batocera-scripts/scripts/batocera-upgrade +++ b/package/batocera/core/batocera-scripts/scripts/batocera-upgrade @@ -213,7 +213,8 @@ do_version_update() { # Upgrade Check, get updated url from ES call do_upgradecheck_0() { local update_url="${1}" - local available=$(wget -qO- ${update_url}) + local available + available=$(wget -qO- ${update_url}) if [ "$?" != "0" ]; then echo "Unable to access the url" >&2 return 2 diff --git a/package/batocera/core/batocera-splash/scripts/Ssplash-mpv b/package/batocera/core/batocera-splash/scripts/Ssplash-mpv index 04cd562433f..6061ade6f1d 100755 --- a/package/batocera/core/batocera-splash/scripts/Ssplash-mpv +++ b/package/batocera/core/batocera-splash/scripts/Ssplash-mpv @@ -204,6 +204,10 @@ do_videostart () fi fi + # set the sound again due to drm init causing some cards to default to 40% volume + VOLUME=$(/usr/bin/batocera-settings-get audio.volume) + batocera-audio setSystemVolume "${VOLUME}" + # On some systems, the graphics driver may take an extra moment to become fully # ready after detection. Check for a user-configurable delay. splash_delay=$(batocera-settings-get splash.delay) @@ -231,30 +235,6 @@ do_videostart () case "$1" in start) printf 'Starting %s: ' "$DAEMON" - - # set the sound again due to drm init causing some cards to default to - # 40% volume. Moved out of do_start() (called below, only when the - # splash video actually plays) - splash.screen.enabled is never "1" - # on a genuinely fresh SD's first boot (see batocera-boot.conf sync - # timing elsewhere in this project), so on that boot do_start() never - # runs at all and this never applied either. This must run on every - # invocation of this script regardless of whether the video plays. - VOLUME=$(/usr/bin/batocera-settings-get audio.volume) - batocera-audio setSystemVolume "${VOLUME}" - - # ODROID-M1 rk809 DAC hardware volume gets the same treatment: - # soundconfig (udev-triggered on the sound card's own probe, well - # before PipeWire starts) already sets it to 252/255 - the highest - # value the driver actually accepts, see soundconfig's own comment - - # but PipeWire/ACP claiming the card later resets it back to the - # driver's power-on default (~80%, -20dB), same timing as the volume - # reset above just affecting the ALSA mixer element PipeWire's own - # volume sits on top of instead of PipeWire's software volume itself. - if [ "$(cat /boot/boot/batocera.board 2>/dev/null)" = "bsp-odroidm1" ]; then - rk809_card=$(awk '/rockchiprk809co/ {print $1; exit}' /proc/asound/cards) - [ -n "${rk809_card}" ] && amixer -c "${rk809_card}" sset DAC 252,252 >/dev/null 2>&1 - fi - if grep -qE '^[ ]*splash.screen.enabled[ ]*=[ ]*1[ ]*$' "/boot/batocera-boot.conf" then do_start diff --git a/package/batocera/core/batocera-system/Config.in b/package/batocera/core/batocera-system/Config.in index 457a8375c33..c38ea4bfda0 100644 --- a/package/batocera/core/batocera-system/Config.in +++ b/package/batocera/core/batocera-system/Config.in @@ -340,16 +340,24 @@ config BR2_PACKAGE_BATOCERA_SYSTEM select BR2_PACKAGE_NEW_LG4FF if !BR2_PACKAGE_BATOCERA_TARGET_XU4 && !BR2_PACKAGE_BATOCERA_TARGET_BCM2835 && !BR2_PACKAGE_BATOCERA_TARGET_RK3326 && !BR2_PACKAGE_BATOCERA_TARGET_RK3128 && !BR2_PACKAGE_BATOCERA_TARGET_QCS6490 && !BR2_PACKAGE_BATOCERA_TARGET_SM6115 && !BR2_PACKAGE_BATOCERA_TARGET_SM8250 && !BR2_PACKAGE_BATOCERA_TARGET_SM8550 && !BR2_PACKAGE_BATOCERA_TARGET_SM8750 && !BR2_PACKAGE_BATOCERA_TARGET_T527 && !BR2_PACKAGE_BATOCERA_TARGET_SDM845 && !BR2_PACKAGE_BATOCERA_TARGET_H700 # Improved Linux module driver for Logitech driving wheels (the lg4ff is disabled in kernels) select BR2_PACKAGE_HID_T150 if !BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_4_4 # Thrustmaster T150 and TMX Force Feedback Wheel Linux drivers select BR2_PACKAGE_HID_TMFF2 if !BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_4_4 # Thrustmaster T300RS, T248 and (experimental) TX, T128, T-GT II and TS-XW Wheel Linux drivers - select BR2_PACKAGE_LOGITECH_TRUEFORCE if BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_1 # Logitech RS50 & G PRO direct drive wheels (force feedback + TrueForce) - select BR2_PACKAGE_LOGI_WHEEL if BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_1 && BR2_PACKAGE_HOST_RUSTC_TARGET_ARCH_SUPPORTS # terminal settings app for the wheels above + # BSP_ODROIDM1 excluded: this board's kernel reports headers 6.1 but is + # a heavily backported hardkernel BSP fork, not vanilla 6.1 - it + # already carries the `const` hid_ll_driver qualifier that mainline + # only added around 6.2+. logitech-trueforce's "less-than-6.2" patch + # codepath assumes 6.1 means pre-const, so it fails to build here with + # -Werror=discarded-qualifiers (hdev->ll_driver = ps->real_ll_driver). + # Same root-cause class as this board's xone kernel-version pin above. + select BR2_PACKAGE_LOGITECH_TRUEFORCE if BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_1 && !BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 # Logitech RS50 & G PRO direct drive wheels (force feedback + TrueForce) + select BR2_PACKAGE_LOGI_WHEEL if BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_1 && BR2_PACKAGE_HOST_RUSTC_TARGET_ARCH_SUPPORTS && !BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 # terminal settings app for the wheels above select BR2_PACKAGE_HID_NX if !BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_7 && !BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_4_4 && !BR2_PACKAGE_BATOCERA_TARGET_BCM2835 && !BR2_PACKAGE_BATOCERA_TARGET_RK3326 # updated nintendo switch controller driver, requires kernel > 4.4 select BR2_PACKAGE_JOYCOND if BR2_PACKAGE_HID_NX || BR2_TOOLCHAIN_HEADERS_AT_LEAST_6_7 # nintendo switch joycon daemon (requires hid-nx or hid-nintendo in kernel 6.7 or later) select BR2_PACKAGE_KEYBOARDTOPADS select BR2_PACKAGE_NSO_N64 if BR2_PACKAGE_HID_NX - select BR2_PACKAGE_ROCKNIX_JOYPAD if BR2_PACKAGE_BATOCERA_TARGET_RK3326 || \ + select BR2_PACKAGE_ROCKNIX_JOYPAD if (BR2_PACKAGE_BATOCERA_TARGET_RK3326 || \ BR2_PACKAGE_BATOCERA_TARGET_RK3568 || \ - BR2_PACKAGE_BATOCERA_TARGET_H700 + BR2_PACKAGE_BATOCERA_TARGET_H700) && \ + !BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 select BR2_PACKAGE_UPOWER # required by some cemuhook servers @@ -799,6 +807,15 @@ config BR2_PACKAGE_BATOCERA_TARGET_JH7110 endchoice +# Board identity for the ODROID-M1 dedicated target (configs/batocera-rk3568-odroidm1.board), +# as opposed to BR2_PACKAGE_UBOOT_ODROID_M1, which the shared 11-board rk3568 +# target (configs/batocera-rk3568.board) also selects - that symbol is a +# bootloader package flag, not a board identity, and using it as a "not this +# board" proxy silently affects the other 10 boards on the shared target too. +config BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 + bool "odroidm1 dedicated bsp target" + default n + config BR2_PACKAGE_BATOCERA_EXTRAS bool "batocera.linux tools packages selection" @@ -1195,7 +1212,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 @@ -1218,7 +1235,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 + select BR2_PACKAGE_TRX if BR2_PACKAGE_XORG7 && BR2_PACKAGE_HAS_LIBGL # Quake 3 select BR2_PACKAGE_IOQUAKE3 if !BR2_PACKAGE_BATOCERA_TARGET_BCM2835 && \ @@ -1446,11 +1463,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 - # newer versions of duckstation are stricter on OpenGL 3.1 - # requires X11 or Sway. 2GB boards aren't great. + !BR2_PACKAGE_BATOCERA_TARGET_S9GEN4 && \ + !(BR2_PACKAGE_MESA3D_HEADERS && BR2_PACKAGE_BATOCERA_VULKAN) select BR2_PACKAGE_DUCKSTATION_LEGACY if !BR2_PACKAGE_BATOCERA_GLES3 && \ !BR2_riscv && \ @@ -2245,7 +2267,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 + select BR2_PACKAGE_SDL3_X11 if BR2_PACKAGE_XORG7 && !BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 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 diff --git a/package/batocera/core/batocera-system/batocera-system.mk b/package/batocera/core/batocera-system/batocera-system.mk index 06ff1db4d40..c7f1af425af 100644 --- a/package/batocera/core/batocera-system/batocera-system.mk +++ b/package/batocera/core/batocera-system/batocera-system.mk @@ -12,6 +12,28 @@ BATOCERA_SYSTEM_DATE = $(shell date "+%Y/%m/%d") BATOCERA_SYSTEM_DEPENDENCIES = tzdata BATOCERA_SYSTEM_INSTALL_IMAGES = YES +# odroid-m1: only override the version string once we actually cut a +# tagged release (batocera44-odroidm1-vX.Y.Z) - before that, keep +# upstream's plain "44-dev" untouched (matches upstream's own dev-build +# identifier exactly; the previously-missing commit hash was purely the +# git-worktree docker mount bug above this file, now fixed, not a reason +# to also rebrand every dev build with an odroid-m1 prefix). +# +# The git tag itself keeps the "batocera44-odroidm1-vX.Y.Z" form (reads +# well in GitHub's tag/release list), but that string can't be used +# as-is for BATOCERA_SYSTEM_VERSION: ES's version label is literally +# "BATOCERA.LINUX ES V" + this value, so the tag's own "batocera"/"v" +# would double up into "...ES Vbatocera44-odroidm1-v1.0.0". Strip those +# down to "44-odroidm1-1.0.0" so the displayed label reads +# "...ES V44-odroidm1-1.0.0", matching the shape of upstream's own +# "V44-dev-". +ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),y) +BATOCERA_SYSTEM_ODROIDM1_RELEASE_TAG := $(shell cd $(BR2_EXTERNAL_BATOCERA_PATH) && git describe --exact-match --tags --match 'batocera44-odroidm1-v*' 2>/dev/null) +ifneq ($(BATOCERA_SYSTEM_ODROIDM1_RELEASE_TAG),) +BATOCERA_SYSTEM_VERSION := $(shell echo $(BATOCERA_SYSTEM_ODROIDM1_RELEASE_TAG) | sed -e 's/^batocera//' -e 's/-v/-/') +endif +endif + ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BCM2837),y) BATOCERA_SYSTEM_ARCH=bcm2837 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3399),y) diff --git a/package/batocera/core/batocera-system/batocera.conf b/package/batocera/core/batocera-system/batocera.conf index 54ba78de5e9..8a72b1cee6d 100644 --- a/package/batocera/core/batocera-system/batocera.conf +++ b/package/batocera/core/batocera-system/batocera.conf @@ -230,6 +230,10 @@ updates.enabled=1 ## stable -> Current stable version ## butterfly -> Current development verion, use at your own risk updates.type=stable +## Custom update manifest - points update checks at our own ODROID-M1 build +## instead of the official batocera.org releases (which this fork tracks +## independently and would otherwise always look "outdated" against). +updates.url=https://raw.githubusercontent.com/Pyohwan/batocera.linux/odroid-m1/updates # ------------ H - Global Emulator Configuration ------------ # diff --git a/package/batocera/emulationstation/batocera-emulationstation/batocera-emulationstation.mk b/package/batocera/emulationstation/batocera-emulationstation/batocera-emulationstation.mk index 2e6d4b3d9c5..419e7392066 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/batocera-emulationstation.mk +++ b/package/batocera/emulationstation/batocera-emulationstation/batocera-emulationstation.mk @@ -226,6 +226,11 @@ define BATOCERA_EMULATIONSTATION_WAYLAND_LABWC $(TARGET_DIR)/usr/bin/labwc-launch endef +define BATOCERA_EMULATIONSTATION_ODROIDM1_INPUT + python3 $(BATOCERA_EMULATIONSTATION_PKGDIR)/controllers/odroidm1_patch_es_input.py \ + $(TARGET_DIR)/usr/share/emulationstation/es_input.cfg +endef + define BATOCERA_EMULATIONSTATION_BOOT $(INSTALL) -D -m 0755 $(BATOCERA_EMULATIONSTATION_PKGDIR)/S31emulationstation \ $(TARGET_DIR)/etc/init.d/S31emulationstation @@ -248,4 +253,16 @@ BATOCERA_EMULATIONSTATION_PRE_CONFIGURE_HOOKS += BATOCERA_EMULATIONSTATION_EXTER BATOCERA_EMULATIONSTATION_POST_INSTALL_TARGET_HOOKS += BATOCERA_EMULATIONSTATION_RESOURCES BATOCERA_EMULATIONSTATION_POST_INSTALL_TARGET_HOOKS += BATOCERA_EMULATIONSTATION_BOOT +## odroid-m1: SHAKS S6b default gamepad mapping + keyboard hotkey==select, +## appended onto whatever upstream's es_input.cfg currently is (not a full +## fsoverlay replacement - this file is upstream's actively-maintained +## controller DB, freezing the whole thing would silently drop every future +## upstream controller mapping addition). Must run after BOOT above, which +## is what actually copies the pristine es_input.cfg to TARGET_DIR - hooks +## run in registration order, not definition order, so registering this +## any earlier gets the patch silently clobbered by that copy. +ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),y) +BATOCERA_EMULATIONSTATION_POST_INSTALL_TARGET_HOOKS += BATOCERA_EMULATIONSTATION_ODROIDM1_INPUT +endif + $(eval $(cmake-package)) diff --git a/package/batocera/emulationstation/batocera-emulationstation/controllers/es_input.cfg b/package/batocera/emulationstation/batocera-emulationstation/controllers/es_input.cfg index de1d620ccdf..b0fcd70a8f8 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/controllers/es_input.cfg +++ b/package/batocera/emulationstation/batocera-emulationstation/controllers/es_input.cfg @@ -9,7 +9,6 @@ - @@ -6266,27 +6265,4 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/package/batocera/emulationstation/batocera-emulationstation/controllers/odroidm1_patch_es_input.py b/package/batocera/emulationstation/batocera-emulationstation/controllers/odroidm1_patch_es_input.py new file mode 100644 index 00000000000..41cd4be98ac --- /dev/null +++ b/package/batocera/emulationstation/batocera-emulationstation/controllers/odroidm1_patch_es_input.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Appends the ODROID-M1-specific additions to the installed es_input.cfg +# (SHAKS S6b default gamepad mapping + keyboard hotkey bound to the same key +# as select) without touching the rest of upstream's controller database, so +# future upstream mapping additions/updates to this file survive untouched. +import sys + +path = sys.argv[1] +with open(path) as f: + text = f.read() + +OLD_KB = '' +assert OLD_KB in text, "es_input.cfg: keyboard select binding not found, upstream layout changed" +assert "SHAKS S6b" not in text, "es_input.cfg: SHAKS S6b entry already present, drop this hook" + +NEW_KB = OLD_KB + '\n\t\t' +text = text.replace(OLD_KB, NEW_KB, 1) + +SHAKS_BLOCK = '''\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t +''' + +CLOSING_TAG = "" +assert text.rstrip().endswith(CLOSING_TAG), "es_input.cfg: unexpected trailing structure, upstream layout changed" +insert_at = text.rstrip().rfind(CLOSING_TAG) +text = text.rstrip()[:insert_at] + SHAKS_BLOCK + CLOSING_TAG + "\n" + +with open(path, "w") as f: + f.write(text) diff --git a/package/batocera/emulationstation/batocera-emulationstation/emulationstation-standalone b/package/batocera/emulationstation/batocera-emulationstation/emulationstation-standalone index 9805e889146..e5d32c0ce8a 100755 --- a/package/batocera/emulationstation/batocera-emulationstation/emulationstation-standalone +++ b/package/batocera/emulationstation/batocera-emulationstation/emulationstation-standalone @@ -41,39 +41,6 @@ _wait_for_compositor() { return 1 } -# Resolves an output identifier to listOutputs' own canonical "N - NAME" -# form, whatever format it was given in. global.videooutput-style values -# ("HDMI-A-1", "DSI-1", matching /sys/class/drm/cardX-) don't match -# listOutputs' "0 - HDMIA" lines (NAME there comes from batocera-drminfo, -# which drops the dashes/suffix) - every validation check below used to -# compare those two formats directly and always fail, so a configured -# global.videooutput was silently treated as invalid every time (falls -# through the whole fallback chain, confirmed live via display.log: -# "Invalid output - HDMI-A-1" even though HDMI-A-1 was in fact connected -# and selectable). Canonicalizing once up front means every subsequent -# exact-string comparison in this script (which already assumes -# listOutputs' own format) just works. -# Caveat: normalization is alnum-lowercase + prefix match, so it can -# mismatch on boards with a connector name that's a strict prefix of -# another (e.g. "DP" vs "DPI" - "dpi1" would match "dp"* first). Not an -# issue on this board (hdmia/dsi share no such prefix relationship). -_canon_output() { - local target="$1" - [ -z "${target}" ] && return 1 - local norm_target - norm_target=$(echo "${target}" | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]') - local line name norm_name - while IFS= read -r line; do - [ "${target}" = "${line}" ] && { echo "${line}"; return 0; } - name="${line#* - }" - norm_name=$(echo "${name}" | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]') - case "${norm_target}" in - "${norm_name}"*) echo "${line}"; return 0 ;; - esac - done < <(batocera-resolution listOutputs) - return 1 -} - GAMELAUNCH=1 while [ -e "${REBOOT_FLAG}" ]; do echo "Standalone: --- Top of display configuration loop ---" >> "$log" @@ -169,36 +136,31 @@ while [ -e "${REBOOT_FLAG}" ]; do # Validate output 1 if [ -n "${settings_output}" ]; then - canon=$(_canon_output "${settings_output}") - if [ -z "${canon}" ]; then + if ! batocera-resolution listOutputs | grep -qE '^'"${settings_output}"'$'; then echo "Standalone: Invalid output - ${settings_output}" >> $log settings_output= - else - settings_output="${canon}" fi fi # Validate output 2 if [ -n "${settings_output2}" ]; then - canon=$(_canon_output "${settings_output2}") - if [ -z "${settings_output}" -o "${settings_output}" = "${canon}" ]; then - settings_output2= - elif [ -z "${canon}" ]; then - echo "Standalone: Invalid output - ${settings_output2}" >> $log + if [ -z "${settings_output}" -o "${settings_output}" = "${settings_output2}" ]; then settings_output2= else - settings_output2="${canon}" + if ! batocera-resolution listOutputs | grep -qE '^'"${settings_output2}"'$'; then + echo "Standalone: Invalid output - ${settings_output2}" >> $log + settings_output2= + fi fi fi # Validate output 3 if [ -n "${settings_output3}" ]; then - canon=$(_canon_output "${settings_output3}") - if [ -z "${settings_output}" -o -z "${settings_output2}" -o "${settings_output}" = "${canon}" -o "${settings_output2}" = "${canon}" ]; then - settings_output3= - elif [ -z "${canon}" ]; then - echo "Standalone: Invalid output - ${settings_output3}" >> $log + if [ -z "${settings_output}" -o -z "${settings_output2}" -o "${settings_output}" = "${settings_output3}" -o "${settings_output2}" = "${settings_output3}" ]; then settings_output3= else - settings_output3="${canon}" + if ! batocera-resolution listOutputs | grep -qE '^'"${settings_output3}"'$'; then + echo "Standalone: Invalid output - ${settings_output3}" >> $log + settings_output3= + fi fi fi @@ -208,9 +170,8 @@ while [ -e "${REBOOT_FLAG}" ]; do fi if [ -z "${settings_output}" ] && [ -n "${LAST_PRIMARY_SCREEN}" ]; then - canon=$(_canon_output "${LAST_PRIMARY_SCREEN}") - if [ -n "${canon}" ]; then - settings_output="${canon}" + if batocera-resolution listOutputs | grep -qE "^${LAST_PRIMARY_SCREEN}$"; then + settings_output="${LAST_PRIMARY_SCREEN}" echo "Standalone: Restored last primary video output - ${settings_output}" >> $log fi fi @@ -219,26 +180,14 @@ while [ -e "${REBOOT_FLAG}" ]; do settings_output=$(batocera-resolution currentOutput) echo "Standalone: First video output defaulted to - ${settings_output}" >> $log # currentOutput may be unplugged after an event of type unplugging it (/var/run/switch_screen_request) - canon=$(_canon_output "${settings_output}") - if [ -z "${canon}" ]; then + if ! batocera-resolution listOutputs | grep -qE "^${settings_output}$"; then echo "Standalone: Default output '${settings_output}' not connected. Finding first available." >> "$log" settings_output=$(batocera-resolution listOutputs | head -1) - else - settings_output="${canon}" fi fi # If screen2 is empty, take the first one found which is not screen1 if [ -z "${settings_output2}" ]; then - # -master (not plain batocera-settings-get): this needs to see - # sysconfig-level defaults too, e.g. ODROID-M1's - # global.videooutput2=none (mutually-exclusive HDMI/VU8M, not - # dual-output). Using the non-master lookup here meant that - # default was invisible - it only checks /userdata/system/ - # batocera.conf directly, so an unset user override read back as - # empty ("!= none") and this auto-picked a second output anyway. - # Confirmed live: DSI-1 got auto-selected and DPMS'd back On every - # boot despite the sysconfig default being in place. - user_output2="$(batocera-settings-get-master global.videooutput2)" + user_output2="$(batocera-settings-get global.videooutput2)" if [ "${user_output2}" != "none" ]; then settings_output2=$(batocera-resolution listOutputs | grep -vE "^${settings_output}$" | head -1) if [ -n "${settings_output2}" ]; then @@ -261,20 +210,6 @@ while [ -e "${REBOOT_FLAG}" ]; do echo "Updated video outputs: ${settings_output}, ${settings_output2}, ${settings_output3}" >> $log ################### - # A previous approach here (DPMS-blanking any connected-but-unused - # output via modetest, to hide it rather than leave it frozen on the - # boot splash) was tried and abandoned: it fought with - # batocera-switch-screen-checker's hotplug detection (every write read - # as a real hotplug, triggering an ES quit/relaunch loop) and, once - # that was fixed, got silently undone again by ES's own screensaver - # every ~5 minutes. Root-caused instead of worked around: the unused - # output only needs hiding at all because our board unconditionally - # probes the VU8M DSI panel regardless of whether it's wanted. Fixed - # at the source in U-Boot (see boot/extlinux.conf's FDTOVERLAYS line, - # present only in the "batocera-vu8m" LABEL) - when VU8M isn't - # opted into, the kernel never probes it, so there's no second - # connector left to hide. - # And now we can apply the setting per output... ### RESOLUTION ### diff --git a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml index 1af62e55b6d..4dc5e3244d9 100644 --- a/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml +++ b/package/batocera/emulationstation/batocera-emulationstation/wayland/labwc/rc.xml @@ -139,7 +139,12 @@ - + + + + + + diff --git a/package/batocera/emulationstation/batocera-es-system/es_systems.yml b/package/batocera/emulationstation/batocera-es-system/es_systems.yml index 1271df987b7..5c4295ed8c3 100644 --- a/package/batocera/emulationstation/batocera-es-system/es_systems.yml +++ b/package/batocera/emulationstation/batocera-es-system/es_systems.yml @@ -215,9 +215,9 @@ n64: extensions: [z64, n64, v64, zip, 7z] emulators: mupen64plus: - gliden64: { requireAnyOf: [BR2_PACKAGE_MUPEN64PLUS_GLIDEN64], incompatible_extensions: [zip, 7z] } - glide64mk2: { requireAnyOf: [BR2_PACKAGE_MUPEN64PLUS_VIDEO_GLIDE64MK2], incompatible_extensions: [zip, 7z] } rice: { requireAnyOf: [BR2_PACKAGE_MUPEN64PLUS_VIDEO_RICE], incompatible_extensions: [zip, 7z] } + glide64mk2: { requireAnyOf: [BR2_PACKAGE_MUPEN64PLUS_VIDEO_GLIDE64MK2], incompatible_extensions: [zip, 7z] } + gliden64: { requireAnyOf: [BR2_PACKAGE_MUPEN64PLUS_GLIDEN64], incompatible_extensions: [zip, 7z] } libretro: mupen64plus-next: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_MUPEN64PLUS_NEXT] } parallel_n64: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_PARALLEL_N64] } @@ -368,14 +368,14 @@ nds: hardware: portable extensions: [nds, bin, zip, 7z] emulators: + drastic: + drastic: { requireAnyOf: [BR2_PACKAGE_DRASTIC], incompatible_extensions: [7z] } libretro: desmume: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_DESMUME] } melonds: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_MELONDS] } melondsds: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_MELONDS_DS] } melonds: melonds: { requireAnyOf: [BR2_PACKAGE_MELONDS] } - drastic: - drastic: { requireAnyOf: [BR2_PACKAGE_DRASTIC], incompatible_extensions: [7z] } fds: name: Family Computer Disk System @@ -434,11 +434,11 @@ dreamcast: hardware: console extensions: [cdi, cue, gdi, chd, m3u] emulators: + flycast: + flycast: { requireAnyOf: [BR2_PACKAGE_FLYCAST] } libretro: flycast: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_FLYCAST] } flycastvl: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_FLYCASTVL] } - flycast: - flycast: { requireAnyOf: [BR2_PACKAGE_FLYCAST] } redream: redream: { requireAnyOf: [BR2_PACKAGE_REDREAM] } comment_en: | @@ -2159,9 +2159,9 @@ saturn: extensions: [cue, ccd, m3u, chd, iso, zip, mds] emulators: libretro: + yabasanshiro: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_YABASANSHIRO], incompatible_extensions: [m3u] } beetle-saturn: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_BEETLE_SATURN], incompatible_extensions: [zip] } kronos: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_KRONOS] } - yabasanshiro: { requireAnyOf: [BR2_PACKAGE_LIBRETRO_YABASANSHIRO], incompatible_extensions: [m3u] } ymir: ymir: { requireAnyOf: [BR2_PACKAGE_YMIR] } @@ -2287,6 +2287,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..1f7236cd504 --- /dev/null +++ b/package/batocera/emulators/aethersx2/Config.in @@ -0,0 +1,43 @@ +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 + # the prebuilt binary NEEDs libXrandr.so.2 (pulled in by Qt's xcb + # platform plugin at link time; never actually called at runtime here + # - aethersx2Generator.py forces QT_QPA_PLATFORM=wayland, so the xcb + # plugin path this satisfies is never taken - same "NEEDED-pass only" + # story as the GL/GLX shims in aethersx2/libs/). Built for real via + # buildroot here rather than vendored like those - libX11/libXext/ + # libXrender/libXrandr have no ABI or missing-feature blocker the way + # our own libcurl/libssl builds do (see aethersx2/libs/README.md), so + # there's no reason not to build them properly. xlib_libXrandr's own + # Config.in already selects XLIB_LIBXEXT/XLIB_LIBXRENDER/XLIB_LIBX11/ + # XORGPROTO, but all of that lives inside "if BR2_PACKAGE_XORG7" + # (=n on this Wayland-only board) - a select landing on a symbol + # there still forces it to y (Kconfig WARNING only), but that + # symbol's own further selects don't reliably cascade past the + # unmet-dependency node (confirmed with melonDS's X11 chain, see + # melonds/Config.in) - flattened here for the same reason. + select BR2_PACKAGE_XLIB_LIBXRANDR + select BR2_PACKAGE_XLIB_LIBXEXT + select BR2_PACKAGE_XLIB_LIBXRENDER + select BR2_PACKAGE_XLIB_LIBX11 + select BR2_PACKAGE_XORGPROTO + 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..13d8739aa1a --- /dev/null +++ b/package/batocera/emulators/aethersx2/aethersx2.emulator.yml @@ -0,0 +1,35 @@ +cores: + aethersx2: + shared_features: + - powermode + - tdp + - videomode + - bezel + - bezel_stretch + - hud + - hud_corner + - use_guns + - use_wheels + custom_features: + aethersx2_gfxbackend: + submenu: RENDERING + prompt: GRAPHICS API + description: Choose which graphics API library to use. Vulkan is the only one confirmed working on this board's GPU driver - OpenGL/Auto will likely fail to start. + choices: + Vulkan: 14 + OpenGL: 12 + Software: 13 + aethersx2_resolution: + submenu: RENDERING + prompt: RENDERING RESOLUTION + description: Enhancement. Increase the rendering resolution. Makes 3D objects clearer. Higher settings need more GPU power. + choices: + Native (PS2): 1 + 1.5x Native: 1.5 + 2x Native (~720p): 2 + 3x Native (~1080p): 3 + 4x Native (~1440p/2K): 4 +systems: + - name: ps2 + exclude_extensions: + - m3u diff --git a/package/batocera/emulators/aethersx2/aethersx2.hash b/package/batocera/emulators/aethersx2/aethersx2.hash new file mode 100644 index 00000000000..aae6ccb9f49 --- /dev/null +++ b/package/batocera/emulators/aethersx2/aethersx2.hash @@ -0,0 +1,2 @@ +# Locally calculated +sha256 b44fe609f2914627c2f9d9dba2513e8f6b72d5679e47b2dadabcd28aa05b8b43 aethersx2.tar.gz diff --git a/package/batocera/emulators/aethersx2/aethersx2.mk b/package/batocera/emulators/aethersx2/aethersx2.mk new file mode 100644 index 00000000000..e52d405eebb --- /dev/null +++ b/package/batocera/emulators/aethersx2/aethersx2.mk @@ -0,0 +1,51 @@ +################################################################################ +# +# 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. +# Pinned to a specific commit rather than the branch HEAD, same as the +# mali-g52-odroidm1 blob below - "main" is a moving target, and this is a +# third-party prebuilt binary with no upstream release/tag to pin to +# instead, so a hash check (aethersx2.hash) matters even more here than +# for source packages we actually compile ourselves. +AETHERSX2_VERSION = 1.5-3606 +AETHERSX2_SOURCE = aethersx2.tar.gz +AETHERSX2_SITE = https://github.com/ROCKNIX/packages/raw/0b61d2a472751b127b1806207d9c459c7d6e334d +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/README.md b/package/batocera/emulators/aethersx2/libs/README.md new file mode 100644 index 00000000000..0c0e056654d --- /dev/null +++ b/package/batocera/emulators/aethersx2/libs/README.md @@ -0,0 +1,81 @@ +# Vendored shim libs + +Pulled from a real, working Debian aarch64 install and live-tested on this +board (see `project_odroid_retro.md`, 2026-08-16, AetherSX2 section) rather +than built from source or fetched fresh at build time, to guarantee they +match what was actually verified working. Installed to +`/usr/aethersx2/libs/` (not the system `/usr/lib/`) and only pulled in via +`aethersx2Generator.py`'s `LD_LIBRARY_PATH=/usr/aethersx2/libs:/usr/lib` - +they never shadow the system's own copies for any other process. + +The `aethersx2` binary link-time-depends on all of these (`NEEDED` entries), +but only the GL/GLX/GLdispatch/OpenGL ones are ever actually called: +`Renderer` is forced to Vulkan in the generator, so real GLX rendering is +never invoked - these four exist purely to satisfy the dynamic linker's +NEEDED pass at process startup. `libpcap` is genuinely unused at runtime +(AetherSX2 has no networking feature enabled in this build) and is only +here for the same NEEDED-pass reason; because it precedes `/usr/lib` in +`LD_LIBRARY_PATH`, it does shadow this board's real `libpcap` +(`BR2_PACKAGE_LIBPCAP`, a newer ABI) for the aethersx2 process specifically +- confirmed intentional, not a conflict, since it's never called. + +**2026-08-22 addition - `libcurl.so.4` + its transitive closure**: launching +aethersx2 failed immediately with `/usr/lib/libcurl.so.4: no version +information available (required by /usr/aethersx2/aethersx2)`. Our own +buildroot-built libcurl (`package/batocera/.../curl`) has no `.gnu.version_d` +at all (built without versioned-symbols support), while the aethersx2 +binary's `.gnu.version_r` hard-requires the `CURL_OPENSSL_4` version node +that only a normally-built (Debian-style) libcurl exports - same class of +problem as the GL/GLX shims above, just one dependency layer deeper. Same +fix: vendor a real Debian aarch64 `libcurl.so.4` (`libcurl4t64` package, +OpenSSL flavour - confirmed via `readelf -V` to define `CURL_OPENSSL_4`), +plus its own `NEEDED` closure that this board doesn't otherwise provide +(nghttp3/idn2/ssh2/gssapi_krb5+krb5+k5crypto+krb5support/ldap+lber/sasl2 - +resolv/keyutils/unistring/com_err/nghttp2/rtmp/psl/ssl/crypto/zstd/brotlidec/z +were all already present on this board and didn't need vendoring). None of +this networking stack is actually exercised at runtime (aethersx2 has no +network feature enabled in this build, same reasoning as `libpcap` above) - +purely a NEEDED-pass satisfaction, not functional network support. + +**Follow-up, same session**: fixing the curl load surfaced a second, same-class +mismatch one layer deeper - this board's own `libssl.so.3`/`libcrypto.so.3` +don't export `ENGINE_init` (`undefined symbol: ENGINE_init, version +OPENSSL_3.0.0`), a legacy OpenSSL 1.1-era API some 3.x builds keep and others +(including this board's) drop. Unlike the curl-closure libs above, curl's +TLS backend init genuinely does call into libssl/libcrypto at process +startup (not just a NEEDED-pass formality), so vendored Debian's +`libssl3t64` here too rather than leaving it as a dangling requirement - +`readelf`/`nm` confirmed the Debian build exports `ENGINE_init`. No further +NEEDED gap: `libssl.so.3`/`libcrypto.so.3` only pull in `libz.so.1`/ +`libzstd.so.1`, both already present. + +Debian package each file's soname maps to (standard Debian/glvnd/mesa/curl +packaging, not independently re-verified against a specific snapshot - +the sha256 below is the actual reproducibility anchor): + +| File | sha256 | Debian package (soname convention) | +| --- | --- | --- | +| `libGL.so.1` | `900229e312e1a5bc67cffdbf89da8e928a8b22713f184154ca7174d457267763` | `libglvnd0` | +| `libGLX.so.0` | `78927ffde410c5a569319c231cc0b8a534aecf781bb3c628e482957c1cf91b83` | `libglvnd0` | +| `libGLdispatch.so.0` | `912ba2ff2d3aa73e6bd579877a462f9541d5d1df4bb9f67435ee4ef16a0f5caa` | `libglvnd0` | +| `libOpenGL.so.0` | `ef81dc02955b9b44f23d83d4c15ed74d26ca74d0489723b6dda3634422018679` | `libglvnd0` | +| `libGLX_mesa.so.0` | `636c5a39b4d3d91cc00a20215f02fea085ea04ba606f42e2f63f7ee24ad8389e` | `libgl1-mesa-dri` / `mesa-libgl` | +| `libpcap.so.0.8` | `697a7f15f258c96aa0e29945507d1cfeb26dd156991a2967bd0ac7761803156f` | `libpcap0.8` | +| `libcurl.so.4` | `2d9cbabde404c2d61ca71a4ff1ae05e38d69e281bc788ac2a21bb10037061149` | `libcurl4t64` (8.14.1-2+deb13u4, OpenSSL flavour) | +| `libnghttp3.so.9` | `82319f388b327a204047d2de787decbd9f592f81e23984d4a1b166306e69667c` | `libnghttp3-9` | +| `libidn2.so.0` | `6ce57885b9ff21ba3fa902c1c515050230d8d6703f12f9a7d1e3d4e27279966e` | `libidn2-0` | +| `libssh2.so.1` | `bc6e431ac9de9f1778cae68407d4b3a0658445b9ee24a010a6d3b5b3246bc63d` | `libssh2-1t64` | +| `libgssapi_krb5.so.2` | `967b9c108b49481f6cc1358d675a6c1b16754254c84c421c43a5814010e2a215` | `libgssapi-krb5-2` | +| `libkrb5.so.3` | `deb1db5877ee7535d8434003404b4a4e181ab660a9e5370ba66c4b8e759e1111` | `libkrb5-3` | +| `libk5crypto.so.3` | `950c7c9c2da1d5ebaae583a6c4da6b9b41bbbee6a4a8f848a868c0e106fe9335` | `libk5crypto3` | +| `libkrb5support.so.0` | `265157c5541d3cc73f5bb5cf43e8a58e996a25211d716a9ab2ba65b1a787633a` | `libkrb5support0` | +| `libldap.so.2` | `52130bdf6f1abdd8b476193cfd532d8c9f3a6d57eeabe06e4edb65b1711be83c` | `libldap2` | +| `liblber.so.2` | `87376cef9c124257cec7fc0b5eac1b4c8535f597c6ebae70ad9334385cfb14d3` | `libldap2` (bundled) | +| `libsasl2.so.2` | `8bfeedd24d410b345ac39c26dfa9970609fe0ca6738a20fd6ac154a232aec152` | `libsasl2-2` | +| `libssl.so.3` | `0cd0e03cfadce973b7ff323db550794f487f3a7bed3b7f43eb23336e7d466647` | `libssl3t64` (3.5.6-1~deb13u2) | +| `libcrypto.so.3` | `d451f843462c144e74c4f11503d3cc894d775deb10d3cfc129fc3aa70cfe8dda` | `libssl3t64` (bundled) | + +All are `aarch64`, stripped, with a BuildID (`readelf -n` / `file `) if +a byte-for-byte source match against a specific Debian snapshot is ever +needed. The curl-closure files are all from Debian 13 (trixie/stable) +`main/binary-arm64`, fetched 2026-08-22. 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 00000000000..f1d9777d570 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libGL.so.1 differ diff --git a/package/batocera/emulators/aethersx2/libs/libGLX.so.0 b/package/batocera/emulators/aethersx2/libs/libGLX.so.0 new file mode 100644 index 00000000000..83d9ff03faa Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libGLX.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/libGLX_mesa.so.0 b/package/batocera/emulators/aethersx2/libs/libGLX_mesa.so.0 new file mode 100644 index 00000000000..4d500d92b51 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libGLX_mesa.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/libGLdispatch.so.0 b/package/batocera/emulators/aethersx2/libs/libGLdispatch.so.0 new file mode 100644 index 00000000000..7e73a2471f0 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libGLdispatch.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/libOpenGL.so.0 b/package/batocera/emulators/aethersx2/libs/libOpenGL.so.0 new file mode 100644 index 00000000000..1a734b042dd Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libOpenGL.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/libcrypto.so.3 b/package/batocera/emulators/aethersx2/libs/libcrypto.so.3 new file mode 100644 index 00000000000..201dfd49e9e Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libcrypto.so.3 differ diff --git a/package/batocera/emulators/aethersx2/libs/libcurl.so.4 b/package/batocera/emulators/aethersx2/libs/libcurl.so.4 new file mode 100644 index 00000000000..78bf4c182af Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libcurl.so.4 differ diff --git a/package/batocera/emulators/aethersx2/libs/libgssapi_krb5.so.2 b/package/batocera/emulators/aethersx2/libs/libgssapi_krb5.so.2 new file mode 100644 index 00000000000..26843dab412 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libgssapi_krb5.so.2 differ diff --git a/package/batocera/emulators/aethersx2/libs/libidn2.so.0 b/package/batocera/emulators/aethersx2/libs/libidn2.so.0 new file mode 100644 index 00000000000..b72a8ef0534 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libidn2.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/libk5crypto.so.3 b/package/batocera/emulators/aethersx2/libs/libk5crypto.so.3 new file mode 100644 index 00000000000..3880174c84d Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libk5crypto.so.3 differ diff --git a/package/batocera/emulators/aethersx2/libs/libkrb5.so.3 b/package/batocera/emulators/aethersx2/libs/libkrb5.so.3 new file mode 100644 index 00000000000..c4c2815518e Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libkrb5.so.3 differ diff --git a/package/batocera/emulators/aethersx2/libs/libkrb5support.so.0 b/package/batocera/emulators/aethersx2/libs/libkrb5support.so.0 new file mode 100644 index 00000000000..2ef21fd728f Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libkrb5support.so.0 differ diff --git a/package/batocera/emulators/aethersx2/libs/liblber.so.2 b/package/batocera/emulators/aethersx2/libs/liblber.so.2 new file mode 100644 index 00000000000..1803437a6e4 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/liblber.so.2 differ diff --git a/package/batocera/emulators/aethersx2/libs/libldap.so.2 b/package/batocera/emulators/aethersx2/libs/libldap.so.2 new file mode 100644 index 00000000000..83e5c7dbde3 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libldap.so.2 differ diff --git a/package/batocera/emulators/aethersx2/libs/libnghttp3.so.9 b/package/batocera/emulators/aethersx2/libs/libnghttp3.so.9 new file mode 100644 index 00000000000..c6c72a2af36 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libnghttp3.so.9 differ diff --git a/package/batocera/emulators/aethersx2/libs/libpcap.so.0.8 b/package/batocera/emulators/aethersx2/libs/libpcap.so.0.8 new file mode 100644 index 00000000000..cf0b8ae7073 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libpcap.so.0.8 differ diff --git a/package/batocera/emulators/aethersx2/libs/libsasl2.so.2 b/package/batocera/emulators/aethersx2/libs/libsasl2.so.2 new file mode 100644 index 00000000000..6de14828a88 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libsasl2.so.2 differ diff --git a/package/batocera/emulators/aethersx2/libs/libssh2.so.1 b/package/batocera/emulators/aethersx2/libs/libssh2.so.1 new file mode 100644 index 00000000000..a793f099a6d Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libssh2.so.1 differ diff --git a/package/batocera/emulators/aethersx2/libs/libssl.so.3 b/package/batocera/emulators/aethersx2/libs/libssl.so.3 new file mode 100644 index 00000000000..703fbb91b04 Binary files /dev/null and b/package/batocera/emulators/aethersx2/libs/libssl.so.3 differ diff --git a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk index 3d822b47693..c0198bbb0f0 100644 --- a/package/batocera/emulators/dolphin-emu/dolphin-emu.mk +++ b/package/batocera/emulators/dolphin-emu/dolphin-emu.mk @@ -3,11 +3,34 @@ # dolphin-emu # ################################################################################ +# On this board (Wayland+Vulkan), 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. +# +# Gated on BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 specifically, not the +# generic BR2_PACKAGE_BATOCERA_WAYLAND (shared by every Wayland Batocera +# board, e.g. x86_64/RK3588/RPi5) - that would silently downgrade dolphin +# and swap all of them onto this older commit's patch directory too. Since +# buildroot's pkg-patches-dirs picks a version-subdir over the flat one +# when both exist, that would also drop the flat-level 005/006/009/011/012 +# patches for every other Wayland board (kept only for the newer commit's +# patch dir below). +ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),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 +40,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 +61,35 @@ 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_TARGET_BSP_ODROIDM1),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 this board must use +# the NoGUI/dolphin-emu-nogui binary regardless of Qt6 availability. +# +# Gated on this board specifically, not generic BR2_PACKAGE_BATOCERA_WAYLAND: +# other Wayland boards now build the newer stock commit (see the +# DOLPHIN_EMU_VERSION swap above), which never gets the Wayland patch +# series in the first place - the "must use NoGUI" reasoning below doesn't +# apply to them, and forcing it anyway silently took away their Qt GUI +# for no reason (2nd review round, PR #4). +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 +# 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 DOLPHIN_EMU_CONF_OPTS += -DENABLE_QT=ON @@ -42,6 +98,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" @@ -52,28 +109,88 @@ else DOLPHIN_EMU_CONF_OPTS += -DENABLE_X11=OFF endif +ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),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. +# +# Board-specific (not generic BR2_PACKAGE_BATOCERA_WAYLAND): a Wayland +# board with a full mesa3d/libGL stack has a real glx.h and may actually +# want X11/XWayland, so this shouldn't force it off for them too. +DOLPHIN_EMU_CONF_OPTS += -DENABLE_X11=OFF +endif + ifeq ($(BR2_PACKAGE_BATOCERA_VULKAN),y) DOLPHIN_EMU_CONF_OPTS += -DENABLE_VULKAN=ON 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 + +ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1),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. +# +# Gated the same as the DOLPHIN_EMU_VERSION swap above (not generic +# BR2_PACKAGE_BATOCERA_WAYLAND): this file only exists in this old +# commit's tree, which only this board builds. +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 + +# The Wayland patch series' PlatformWayland.cpp includes UICommon/X11Utils.h +# for no real reason (zero X11Utils:: symbols used - confirmed via source +# read) EXCEPT that its two GFX_SHOW_FPS toggle calls sloppily pass Xlib's +# True/False macros (#define True 1 / #define False 0) instead of C++ +# true/false for an Info param - they happen to compile because the +# macros expand to int literals that convert to bool. That header's own +# #include is unconditional (not gated on HAVE_X11), so with +# ENABLE_X11=OFF above (no X11 dev headers in this board's sysroot) the +# include fails outright. Fix is two parts: drop the vestigial include, and +# replace the two True/False literals with true/false so nothing here +# depends on Xlib.h at all. +define DOLPHIN_EMU_FIX_WAYLAND_X11_INCLUDE + sed -i '/#include "UICommon\/X11Utils.h"/d' \ + $(@D)/Source/Core/DolphinNoGUI/PlatformWayland.cpp + sed -i \ + -e 's/Config::SetCurrent(Config::GFX_SHOW_FPS, True);/Config::SetCurrent(Config::GFX_SHOW_FPS, true);/' \ + -e 's/Config::SetCurrent(Config::GFX_SHOW_FPS, False);/Config::SetCurrent(Config::GFX_SHOW_FPS, false);/' \ + $(@D)/Source/Core/DolphinNoGUI/PlatformWayland.cpp +endef +DOLPHIN_EMU_PRE_CONFIGURE_HOOKS += DOLPHIN_EMU_FIX_WAYLAND_X11_INCLUDE +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; 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), 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([] { diff --git a/package/batocera/emulators/dolphin-emu/gfxbackend.dolphin.emulator.yml b/package/batocera/emulators/dolphin-emu/gfxbackend.dolphin.emulator.yml index 080cd16afeb..73b1c4b9486 100644 --- a/package/batocera/emulators/dolphin-emu/gfxbackend.dolphin.emulator.yml +++ b/package/batocera/emulators/dolphin-emu/gfxbackend.dolphin.emulator.yml @@ -6,6 +6,6 @@ cores: prompt: GRAPHICS API description: Choose which graphics API library to use. choices: - OpenGL: OGL Vulkan: Vulkan + OpenGL: OGL before: internal_resolution diff --git a/package/batocera/emulators/duckstation/Config.in b/package/batocera/emulators/duckstation/Config.in index 5e4f5e6e884..1c39f3c44b1 100644 --- a/package/batocera/emulators/duckstation/Config.in +++ b/package/batocera/emulators/duckstation/Config.in @@ -1,5 +1,13 @@ config BR2_PACKAGE_DUCKSTATION bool "duckstation" + # 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 diff --git a/package/batocera/emulators/melonds/005-frametime-log.patch b/package/batocera/emulators/melonds/005-frametime-log.patch new file mode 100644 index 00000000000..ce0730b70cf --- /dev/null +++ b/package/batocera/emulators/melonds/005-frametime-log.patch @@ -0,0 +1,37 @@ +--- a/src/frontend/qt_sdl/Screen.cpp ++++ b/src/frontend/qt_sdl/Screen.cpp +@@ -17,6 +17,8 @@ + */ + + #include ++#include ++#include + + #include + #include +@@ -1256,6 +1258,25 @@ + } + + glContext->SwapBuffers(); ++ ++ /* frametime logger (env FRAMETIME_LOG) */ ++ { ++ static FILE *ft_log = nullptr; static bool ft_init = false; ++ if (!ft_init) ++ { ++ const char *p = getenv("FRAMETIME_LOG"); ++ if (p) ++ ft_log = fopen(p, "w"); ++ ft_init = true; ++ } ++ if (ft_log) ++ { ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ fprintf(ft_log, "%lld\n", ++ (long long)(ts.tv_sec * 1000000LL + ts.tv_nsec / 1000)); ++ } ++ } + } + + qreal ScreenPanelGL::devicePixelRatioFromScreen() const diff --git a/package/batocera/emulators/melonds/Config.in b/package/batocera/emulators/melonds/Config.in index 36a9da710dd..2c4a86b60b1 100644 --- a/package/batocera/emulators/melonds/Config.in +++ b/package/batocera/emulators/melonds/Config.in @@ -7,6 +7,25 @@ config BR2_PACKAGE_MELONDS select BR2_PACKAGE_LIBEPOXY select BR2_PACKAGE_SDL2 select BR2_PACKAGE_SLIRP + # xlib_libX11 and its own x11r7 Config.in selects (libxcb, xtrans, + # libXau, libXdmcp) all live inside "if BR2_PACKAGE_XORG7" - on this + # Wayland-only board XORG7=n, so a select landing on one of these + # symbols still forces it to y (just a Kconfig WARNING, confirmed via + # a real olddefconfig run), but that forced symbol's OWN outgoing + # selects do not reliably cascade any further (multi-hop select + # through an unmet-dependency node breaks here - confirmed: selecting + # only XLIB_LIBX11 left libxcb/xtrans/libXau/libXdmcp unset even + # though xlib_libX11's Config.in selects all four). Flattened to a + # direct select list so every one of them is forced straight from this + # (cleanly enabled) MELONDS symbol instead of relying on the chain. + select BR2_PACKAGE_XLIB_LIBX11 + select BR2_PACKAGE_LIBXCB + select BR2_PACKAGE_XCB_PROTO + select BR2_PACKAGE_XLIB_XTRANS + select BR2_PACKAGE_XLIB_LIBXAU + select BR2_PACKAGE_XLIB_LIBXDMCP + select BR2_PACKAGE_XUTIL_UTIL_MACROS + select BR2_PACKAGE_XORGPROTO help A Ninteno DS emulator diff --git a/package/batocera/emulators/melonds/melonds.mk b/package/batocera/emulators/melonds/melonds.mk index 570a46f48bb..961308aace4 100644 --- a/package/batocera/emulators/melonds/melonds.mk +++ b/package/batocera/emulators/melonds/melonds.mk @@ -12,20 +12,67 @@ MELONDS_LICENSE = GPLv2 MELONDS_EMULATOR_INFO = melonds.emulator.yml MELONDS_DEPENDENCIES += ecm sdl2 slirp libepoxy libarchive libenet MELONDS_DEPENDENCIES += qt6base qt6svg qt6multimedia +# melonDS's Qt frontend does an unconditional find_package(X11 REQUIRED) on +# Linux (src/frontend/qt_sdl/CMakeLists.txt), regardless of ENABLE_WAYLAND - +# this is a build-time link dependency only, not a runtime X server +# requirement (this board has no Xorg/XWayland running at all, same +# xlib_libX11-as-build-dep pattern already used by raze/gzdoom/moonlight-qt). +MELONDS_DEPENDENCIES += xlib_libX11 -# wayland is currently broken, don't set this. This board (odroid-m1) was -# briefly special-cased to ON here on the assumption that Wayland was the -# only option since it has no XWayland - live-tested and wrong: the vendor -# Mali blob's Wayland-*client* EGL platform is itself broken on this board -# (eglGetPlatformDisplayEXT succeeds, eglInitialize always fails with -# EGL_NOT_INITIALIZED - confirmed on both g13p0 and g24p0 blob versions, -# see board defconfig comment / Joplin "GPU 렌더링 경로" note), unrelated to -# desktop GL availability. The actual working path on this board is Qt's -# "eglfs" platform plugin (direct KMS/GBM, same EGL platform every other -# consumer on this board already uses successfully) via QT_QPA_PLATFORM= -# eglfs at runtime - which needs libinput (added above) for its KMS input -# backend, nothing Wayland-related at all. +MELONDS_SUPPORTS_IN_SOURCE_BUILD = NO + +MELONDS_CONF_OPTS += -DCMAKE_BUILD_TYPE=Release +MELONDS_CONF_OPTS += -DCMAKE_INSTALL_PREFIX="/usr" +MELONDS_CONF_OPTS += -DBUILD_SHARED_LIBS=OFF +MELONDS_CONF_OPTS += -DUSE_QT6=ON + +# Upstream's own "wayland is currently broken" comment predates this +# board's setup and applied generically; qt6wayland is already built and +# working here (dolphin-emu's native Wayland platform depends on it), so +# use native Wayland for melonDS's GL window handle path (needed for the +# dual-screen ES feature - see Screen.cpp fix below for why this alone +# isn't enough to build cleanly). +ifeq ($(BR2_PACKAGE_QT6WAYLAND),y) +MELONDS_CONF_OPTS += -DENABLE_WAYLAND=ON +MELONDS_DEPENDENCIES += qt6wayland wayland wayland-protocols libxkbcommon +else MELONDS_CONF_OPTS += -DENABLE_WAYLAND=OFF +endif + +# src/frontend/qt_sdl/Screen.cpp's getWindowInfo() unconditionally +# references QX11Application (Qt6 >= 6.5's xcb-only native interface +# class) in an `if (platformName() == "xcb")` branch that isn't gated on +# ENABLE_WAYLAND at all - it's checked purely by QT_VERSION. This board's +# qt6base has no xcb feature (BR2_PACKAGE_QT6BASE_XCB unset, Wayland-only, +# no X11 libs in target sysroot beyond the build-time-only xlib_libX11 +# above), so QX11Application doesn't exist and the type fails to compile +# even though the branch is unreachable at runtime here (platformName() +# is always "wayland" on this board). Turning on xcb support just to +# satisfy dead code would mean rebuilding all of qt6base (one of the +# largest packages in this build) - drop the xcb branch instead and make +# the wayland branch (which already exists and does the real work here) +# unconditional. Confirmed no other file references QX11Application. +# +# Second, separate upstream bug found while fixing the above: the wayland +# branch is guarded by `#if defined(ENABLE_WAYLAND) && ENABLE_WAYLAND`, +# but CMakeLists.txt (this same source tree, line ~148) only ever defines +# a *differently-named* macro, WAYLAND_ENABLED (target_compile_definitions +# ... WAYLAND_ENABLED) - confirmed this is the real macro via +# duckstation/gl/context.cpp's own `#ifdef WAYLAND_ENABLED` usage. So the +# wayland branch was dead code even in unpatched upstream melonDS +# regardless of the CMake ENABLE_WAYLAND option's value - fixed to check +# the macro CMake actually defines. +define MELONDS_FIX_WAYLAND_ONLY_WINDOWINFO + python3 -c "\ +p = '$(@D)/src/frontend/qt_sdl/Screen.cpp'; \ +s = open(p).read(); \ +old = ''' #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n if (platform_name == QStringLiteral(\"xcb\"))\n {\n wi.type = WindowInfo::Type::X11;\n const QX11Application* x11 = qApp->nativeInterface();\n wi.display_connection = x11->display();\n wi.window_handle = reinterpret_cast(winId());\n }\n #if defined(ENABLE_WAYLAND) && ENABLE_WAYLAND\n else if (platform_name == QStringLiteral(\"wayland\"))\n'''; \ +new = ''' #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)\n #if defined(WAYLAND_ENABLED)\n if (platform_name == QStringLiteral(\"wayland\"))\n'''; \ +assert old in s, 'melonDS Screen.cpp xcb block not found - upstream source changed, review this patch'; \ +open(p, 'w').write(s.replace(old, new, 1))" +endef +MELONDS_PRE_CONFIGURE_HOOKS += MELONDS_FIX_WAYLAND_ONLY_WINDOWINFO + define MELONDS_INSTALL_TARGET_CMDS $(INSTALL) -D $(@D)/buildroot-build/melonDS \ $(TARGET_DIR)/usr/bin/ 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: "); diff --git a/package/batocera/emulators/pcsx2/Config.in b/package/batocera/emulators/pcsx2/Config.in index 314191b0e97..e8dafd7f738 100644 --- a/package/batocera/emulators/pcsx2/Config.in +++ b/package/batocera/emulators/pcsx2/Config.in @@ -1,7 +1,16 @@ config BR2_PACKAGE_PCSX2 bool "pcsx2" depends on BR2_x86_64 || BR2_aarch64 - depends on BR2_PACKAGE_XORG7 + # BSP_ODROIDM1 excluded from the XORG7 requirement: this board is + # Wayland-only (no X11 anywhere in the stack), and PCSX2's Qt6 UI + # already runs here via the same Wayland/eglfs fallback pattern used + # by duckstation-qt/dolphin-emu/AetherSX2 on this board - confirmed + # building and running (GS thread hang + swapchain double-free/race + # bugs found and fixed independently of X11, see project history). + # Without this, an origin/master sync silently reintroduced this + # dependency and Kconfig's olddefconfig step drops the .board's + # forced BR2_PACKAGE_PCSX2=y since XORG7 isn't enabled on this board. + depends on BR2_PACKAGE_XORG7 || BR2_PACKAGE_BATOCERA_TARGET_BSP_ODROIDM1 select BR2_PACKAGE_ALSA_LIB select BR2_PACKAGE_BATOCERA_QT6 select BR2_PACKAGE_BR2_PACKAGE_FMT @@ -22,11 +31,16 @@ config BR2_PACKAGE_PCSX2 select BR2_PACKAGE_RAPIDYAML select BR2_PACKAGE_SDL3 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_VULKAN_HEADERS if BR2_PACKAGE_BATOCERA_VULKAN + select BR2_PACKAGE_VULKAN_LOADER if BR2_PACKAGE_BATOCERA_VULKAN + # Header-only package (protocol headers, no runtime X11 dependency) - + # was previously guaranteed enabled transitively via XORG7, which this + # board no longer depends on above. select it directly instead of + # relying on that chain. + select BR2_PACKAGE_XORGPROTO + 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..524cfcbd45e 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 @@ -49,6 +53,48 @@ define PCSX2_FIX_WHOLE_ARCHIVE endef PCSX2_PRE_CONFIGURE_HOOKS += PCSX2_FIX_WHOLE_ARCHIVE +# pcsx2-qt/QtUtils.cpp includes QtGui/private/qtx11extras_p.h under a bare +# #ifdef Q_OS_LINUX (not gated on the X11_API CMake option below at all), +# so it still breaks with X11_API=OFF on this xcb-less Qt6 (Wayland-only +# board, same-class fix as melonDS/kddockwidgets). The only use +# (QX11Info::isPlatformX11() inside IsCompositorManagerRunning()) is +# already runtime-dead here, so drop both instead of rebuilding qt6base +# with xcb just for unreachable code. +define PCSX2_FIX_X11EXTRAS_INCLUDE + python3 -c "\ +p = '$(@D)/pcsx2-qt/QtUtils.cpp'; \ +s = open(p).read(); \ +old_include = '''#ifdef Q_OS_LINUX\n#include \n#endif\n'''; \ +old_branch = '''#ifdef Q_OS_LINUX\n\t\tif (QX11Info::isPlatformX11() && !QX11Info::isCompositingManagerRunning())\n\t\t\treturn false;\n#endif\n\n'''; \ +assert s.count(old_include) == 1 and s.count(old_branch) == 1, 'pcsx2 QtUtils.cpp X11 block not found - upstream source changed, review this patch'; \ +s = s.replace(old_include, '', 1).replace(old_branch, '', 1); \ +open(p, 'w').write(s)" +endef +PCSX2_PRE_CONFIGURE_HOOKS += PCSX2_FIX_X11EXTRAS_INCLUDE + +# common/Linux/LnxMisc.cpp wraps Common::SetMousePosition/ +# AttachMousePositionCb/DetachMousePositionCb entirely in +# #if defined(X11_API) with no non-X11 Linux fallback (unlike macOS/ +# Windows, which both have real implementations) - upstream simply never +# added a Wayland/generic-Linux path for this X11-only mouse-warp/raw- +# motion feature. With X11_API=OFF on this board the symbols don't exist +# at all, but MainWindow.cpp/InterfaceSettingsWidget.cpp reference them +# unconditionally, so the link fails. Add the same "unsupported" stub +# contract the callers already handle gracefully (AttachMousePositionCb +# returning false just logs a warning - see MainWindow.cpp's +# setupMouseMoveHandler()); this only disables lightgun-style mouse-warp +# aiming, not the emulator itself. +define PCSX2_FIX_LINUX_MOUSE_POSITION_STUB + python3 -c "\ +p = '$(@D)/common/Linux/LnxMisc.cpp'; \ +s = open(p).read(); \ +old = '#endif // X11_API\n'; \ +assert s.count(old) == 1, 'pcsx2 LnxMisc.cpp X11_API block not found - upstream source changed, review this patch'; \ +new = '''#else\nvoid Common::SetMousePosition(int x, int y)\n{\n}\n\nbool Common::AttachMousePositionCb(std::function cb)\n{\n\treturn false;\n}\n\nvoid Common::DetachMousePositionCb()\n{\n}\n#endif // X11_API\n'''; \ +open(p, 'w').write(s.replace(old, new, 1))" +endef +PCSX2_PRE_CONFIGURE_HOOKS += PCSX2_FIX_LINUX_MOUSE_POSITION_STUB + ifeq ($(BR2_PACKAGE_XORG7),y) PCSX2_CONF_OPTS += -DX11_API=ON else diff --git a/package/batocera/emulators/ppsspp/gfxbackend.ppsspp.emulator.yml b/package/batocera/emulators/ppsspp/gfxbackend.ppsspp.emulator.yml index c7c051d9541..2ceab845982 100644 --- a/package/batocera/emulators/ppsspp/gfxbackend.ppsspp.emulator.yml +++ b/package/batocera/emulators/ppsspp/gfxbackend.ppsspp.emulator.yml @@ -4,6 +4,6 @@ custom_features: prompt: GRAPHICS API description: Choose which graphics API library to use. choices: - OpenGL: 0 (OPENGL) Vulkan: 3 (VULKAN) + OpenGL: 0 (OPENGL) before: internal_resolution 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 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)" \ diff --git a/package/batocera/emulators/retroarch/libretro/libretro-beetle-saturn/libretro-beetle-saturn.mk b/package/batocera/emulators/retroarch/libretro/libretro-beetle-saturn/libretro-beetle-saturn.mk index 721bd1972d2..3299022fa40 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-beetle-saturn/libretro-beetle-saturn.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-beetle-saturn/libretro-beetle-saturn.mk @@ -11,13 +11,20 @@ LIBRETRO_BEETLE_SATURN_LICENSE = GPLv2 LIBRETRO_BEETLE_SATURN_DEPENDENCIES += retroarch LIBRETRO_BEETLE_SATURN_EMULATOR_INFO = beetle-saturn.libretro.core.yml +LIBRETRO_BEETLE_SATURN_PLATFORM = unix + +# This board only has GLES (mesa3d-headers, no desktop GL/GLX) - the +# upstream Makefile's HAVE_OPENGL=1 path links against desktop -lGL unless +# "gles" appears in the platform string, in which case it switches to +# -lGLESv2 instead (same pattern as libretro-beetle-psx's "unix-gles"). ifeq ($(BR2_PACKAGE_HAS_LIBGLES),y) LIBRETRO_BEETLE_SATURN_DEPENDENCIES += libgles +LIBRETRO_BEETLE_SATURN_PLATFORM = unix-gles endif define LIBRETRO_BEETLE_SATURN_BUILD_CMDS $(TARGET_CONFIGURE_OPTS) $(MAKE) CXX="$(TARGET_CXX)" CC="$(TARGET_CC)" \ - -C $(@D) -f Makefile HAVE_OPENGL=1 platform="$(LIBRETRO_PLATFORM)" + -C $(@D) -f Makefile HAVE_OPENGL=1 platform="$(LIBRETRO_BEETLE_SATURN_PLATFORM)" endef define LIBRETRO_BEETLE_SATURN_INSTALL_TARGET_CMDS 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 diff --git a/package/batocera/emulators/retroarch/libretro/libretro-fbneo-korean/libretro-fbneo-korean.mk b/package/batocera/emulators/retroarch/libretro/libretro-fbneo-korean/libretro-fbneo-korean.mk index 4fc1d0a253c..5406c38405f 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-fbneo-korean/libretro-fbneo-korean.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-fbneo-korean/libretro-fbneo-korean.mk @@ -3,7 +3,7 @@ # libretro-fbneo-korean # ################################################################################ -# dsno/Zansword's Korean-patched FBNeo fork (CRC-tolerant driver patches for +# DsNo's Korean-patched FBNeo fork (CRC-tolerant driver patches for # Korean-translated arcade ROMs, standard libretro-fbneo rejects these on # CRC32 mismatch) mirrored to a personal fork for build reproducibility - # the original only existed as an uncommitted working tree on a dev machine. diff --git a/package/batocera/emulators/retroarch/libretro/libretro-kronos/libretro-kronos.mk b/package/batocera/emulators/retroarch/libretro/libretro-kronos/libretro-kronos.mk index 0cb9c22d16b..c392afdae5e 100644 --- a/package/batocera/emulators/retroarch/libretro/libretro-kronos/libretro-kronos.mk +++ b/package/batocera/emulators/retroarch/libretro/libretro-kronos/libretro-kronos.mk @@ -29,15 +29,6 @@ LIBRETRO_KRONOS_EXTRA_ARGS += FORCE_GLES=1 else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_S905GEN3),y) LIBRETRO_KRONOS_PLATFORM = odroid-c4 LIBRETRO_KRONOS_EXTRA_ARGS += FORCE_GLES=1 - -else ifeq ($(BR2_PACKAGE_BATOCERA_TARGET_RK3568),y) -# upstream only selects this package for x86 (see Config.in) - never tested -# on RK3568, but the .mk itself already supports several other ARM+GLES -# boards (odroid-xu4/n2, rockpro64, odroid-c4) the same way, so reusing the -# odroid-c4 GLES platform here is a reasonable first attempt, not a known- -# good config. -LIBRETRO_KRONOS_PLATFORM = odroid-c4 -LIBRETRO_KRONOS_EXTRA_ARGS += FORCE_GLES=1 endif define LIBRETRO_KRONOS_BUILD_CMDS 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 diff --git a/package/batocera/emulators/retroarch/retroarch/900-frametime-log.patch b/package/batocera/emulators/retroarch/retroarch/900-frametime-log.patch new file mode 100644 index 00000000000..6f702422043 --- /dev/null +++ b/package/batocera/emulators/retroarch/retroarch/900-frametime-log.patch @@ -0,0 +1,33 @@ +--- a/gfx/video_driver.c ++++ b/gfx/video_driver.c +@@ -16,6 +16,8 @@ + + #include + #include ++#include ++#include + + #include + #include +@@ -3835,6 +3837,21 @@ + if (!video_driver_active) + return; + ++ /* frametime logger (env FRAMETIME_LOG) */ ++ { ++ static FILE *ft_log; static bool ft_init; ++ if (!ft_init) ++ { ++ const char *p = getenv("FRAMETIME_LOG"); ++ if (p) ++ ft_log = fopen(p, "w"); ++ ft_init = true; ++ } ++ if (ft_log) ++ fprintf(ft_log, "%" PRId64 ",%d\n", ++ cpu_features_get_time_usec(), data != NULL); ++ } ++ + new_time = cpu_features_get_time_usec(); + runloop_st->core_run_time = new_time - runloop_st->core_run_time; + 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 5b6bbefe2c9..5b40c2545c1 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 @@ -92,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; \ @@ -111,19 +122,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 @@ -155,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" + } +} 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..f66c8436407 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0001-fall-back-to-implicit-sync.patch @@ -0,0 +1,214 @@ +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 +@@ -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 +339,11 @@ static bool check_wl_protocols(struct wl_display *display) + return false; + } + +- 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 ++++ 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/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; diff --git a/package/batocera/gpu/vulkan/vulkan-wsi-layer/0003-frametime-log.patch b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0003-frametime-log.patch new file mode 100644 index 00000000000..de66481a4c2 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/0003-frametime-log.patch @@ -0,0 +1,33 @@ +--- a/layer/swapchain_api.cpp ++++ b/layer/swapchain_api.cpp +@@ -30,6 +30,8 @@ + + #include + #include ++#include ++#include + #include + + #include "private_data.hpp" +@@ -169,6 +171,21 @@ + assert(queue != VK_NULL_HANDLE); + assert(pPresentInfo != nullptr); + ++ /* frametime logger (env FRAMETIME_LOG_VK) */ ++ { ++ static FILE *ft_log = [] { ++ const char *p = getenv("FRAMETIME_LOG_VK"); ++ return p ? fopen(p, "w") : nullptr; ++ }(); ++ if (ft_log) ++ { ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ fprintf(ft_log, "%lld\n", ++ (long long)(ts.tv_sec * 1000000LL + ts.tv_nsec / 1000)); ++ } ++ } ++ + layer::device_private_data &device_data = layer::device_private_data::get(queue); + + if (!device_data.layer_owns_all_swapchains(pPresentInfo->pSwapchains, pPresentInfo->swapchainCount)) 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..a7ea4684743 --- /dev/null +++ b/package/batocera/gpu/vulkan/vulkan-wsi-layer/vulkan-wsi-layer.mk @@ -0,0 +1,90 @@ +################################################################################ +# +# 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"). 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. +# 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 during the initial bring-up +# investigation - buildroot's cmake-package defaults to +# CMAKE_BUILD_TYPE=Release (-DNDEBUG). CMAKE_BUILD_TYPE=Debug was forced +# here until the Vulkan surface/swapchain path was confirmed stable +# end-to-end (now true - full system matrix tested clean, see +# project_odroid_retro.md) - reverted to buildroot's default Release since +# this layer sits in every Wayland Vulkan present on this board, and a +# Debug build's extra logging isn't valid ground for A/B/C fps comparison. +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)) diff --git a/package/batocera/kodi/kodi21-resource-language-it_it/kodi21_resource_language_it_it.mk b/package/batocera/kodi/kodi21-resource-language-it_it/kodi21_resource_language_it_it.mk index 65130fc0a3f..fe4d5121e36 100644 --- a/package/batocera/kodi/kodi21-resource-language-it_it/kodi21_resource_language_it_it.mk +++ b/package/batocera/kodi/kodi21-resource-language-it_it/kodi21_resource_language_it_it.mk @@ -4,7 +4,7 @@ # ################################################################################ -KODI21_RESOURCE_LANGUAGE_IT_IT_VERSION = 11.0.106 +KODI21_RESOURCE_LANGUAGE_IT_IT_VERSION = 11.0.109 KODI21_RESOURCE_LANGUAGE_IT_IT_SOURCE = \ resource.language.it_it-$(KODI21_RESOURCE_LANGUAGE_IT_IT_VERSION).zip KODI21_RESOURCE_LANGUAGE_IT_IT_SITE = \ diff --git a/package/batocera/libraries/kddockwidgets/kddockwidgets.mk b/package/batocera/libraries/kddockwidgets/kddockwidgets.mk index d07f238412d..27d4b8e8636 100644 --- a/package/batocera/libraries/kddockwidgets/kddockwidgets.mk +++ b/package/batocera/libraries/kddockwidgets/kddockwidgets.mk @@ -21,4 +21,26 @@ KDDOCKWIDGETS_CONF_OPTS += -DKDDockWidgets_QT6=ON KDDOCKWIDGETS_CONF_OPTS += -DKDDockWidgets_EXAMPLES=OFF KDDOCKWIDGETS_CONF_OPTS += -DECM_MKSPECS_INSTALL_DIR=/usr/mkspecs/modules +# src/qtwidgets/views/ClassicIndicatorsWindow.cpp includes +# QtGui/private/qtx11extras_p.h whenever QT_VERSION>=6 && Linux, regardless +# of whether Qt6 actually has xcb support built in. This board's qt6base +# has no xcb feature (Wayland-only, same reasoning as melonDS's +# MELONDS_FIX_WAYLAND_ONLY_WINDOWINFO fix), so the header doesn't exist. +# The only symbol from it (QX11Info::isCompositingManagerRunning) is +# already runtime-guarded by isXCB() (always false on Wayland), so drop +# the include and that dead branch instead of rebuilding qt6base with +# xcb just to satisfy unreachable code. +define KDDOCKWIDGETS_FIX_X11EXTRAS_INCLUDE + python3 -c "\ +p = '$(@D)/src/qtwidgets/views/ClassicIndicatorsWindow.cpp'; \ +s = open(p).read(); \ +old_include = '''#ifdef QT_X11EXTRAS_LIB\n#include \n#elif QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && defined(Q_OS_LINUX)\n#include \n#endif\n'''; \ +old_branch = '''#if defined(QT_X11EXTRAS_LIB) || (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && defined(Q_OS_LINUX))\n if (isXCB())\n return QX11Info::isCompositingManagerRunning();\n#endif\n\n // macOS and Windows are fine\n return true;\n'''; \ +new_branch = ''' // macOS, Windows, and Wayland (this board) are fine\n return true;\n'''; \ +assert old_include in s and old_branch in s, 'kddockwidgets ClassicIndicatorsWindow.cpp X11 block not found - upstream source changed, review this patch'; \ +s = s.replace(old_include, '', 1).replace(old_branch, new_branch, 1); \ +open(p, 'w').write(s)" +endef +KDDOCKWIDGETS_PRE_CONFIGURE_HOOKS += KDDOCKWIDGETS_FIX_X11EXTRAS_INCLUDE + $(eval $(cmake-package)) 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 diff --git a/package/batocera/screens/batocera-backglass/batocera-backglass-window.cpp b/package/batocera/screens/batocera-backglass/batocera-backglass-window.cpp index 16c45676706..ef43fe2c8cd 100644 --- a/package/batocera/screens/batocera-backglass/batocera-backglass-window.cpp +++ b/package/batocera/screens/batocera-backglass/batocera-backglass-window.cpp @@ -557,8 +557,17 @@ int main(int argc, char* argv[]) { if (TTF_Init() < 0) return 1; // Separate Bold (Headers) from Regular weight (Descriptions) + // Noto Sans KR checked first in both lists below: it's a variable font + // with full Latin+CJK coverage in one file (Google Noto design goal), + // so using it as the primary choice (not just a Korean-only fallback) + // renders both scripts consistently without per-glyph font switching, + // which SDL_ttf/TTF_RenderUTF8_Blended_Wrapped can't do anyway (one + // font per call). Without this, Korean text (game titles, backglass + // metadata) silently rendered as missing-glyph boxes - DejaVu/ + // Liberation below have no CJK glyphs at all. std::string font_path_header = ""; std::vector header_font_candidates = { + "/usr/share/fonts/truetype/noto/NotoSansKR-VF.ttf", "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", "/usr/share/fonts/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", @@ -574,6 +583,7 @@ int main(int argc, char* argv[]) { std::string font_path_desc = ""; std::vector desc_font_candidates = { + "/usr/share/fonts/truetype/noto/NotoSansKR-VF.ttf", "/usr/share/fonts/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", diff --git a/package/batocera/utils-host/cargo-c/cargo-c.mk b/package/batocera/utils-host/cargo-c/cargo-c.mk index 9bf4690bbd4..5c9bd2a242d 100644 --- a/package/batocera/utils-host/cargo-c/cargo-c.mk +++ b/package/batocera/utils-host/cargo-c/cargo-c.mk @@ -4,8 +4,18 @@ # ################################################################################ -CARGO_C_VERSION = v0.10.19 -CARGO_C_SITE = $(call github,lu-zero,cargo-c,$(CARGO_C_VERSION)) +# Pinned to our fork's odroidm1-rustc1.95-pin branch tip instead of +# upstream v0.10.19 directly: cargo-c ships no Cargo.lock, so buildroot's +# cargo-vendor download step always resolves transitive deps to whatever +# is newest on crates.io at build time. kstring (pulled in via +# toml -> toml_edit) bumped its MSRV to rustc 1.96 in 2.0.3, which breaks +# a fresh vendor against this buildroot's host-rustc (pinned at 1.95.0, +# see buildroot/package/rust/rust.mk) with "rustc 1.95.0 is not supported +# by kstring@2.0.4". Our fork just adds an explicit kstring = "=2.0.2" +# dependency to Cargo.toml so the vendor resolves to an MSRV-1.73 +# release instead - see https://github.com/Pyohwan/cargo-c/commit/3ddf1eb +CARGO_C_VERSION = 3ddf1ebbc6fe4df186e5ad7488cdaeaaeaa6e046 +CARGO_C_SITE = $(call github,Pyohwan,cargo-c,$(CARGO_C_VERSION)) CARGO_C_LICENSE = MIT License CARGO_C_LICENSE_FILES = LICENSE diff --git a/package/batocera/utils/mangohud/001-fix-vulakan-and-x11.patch b/package/batocera/utils/mangohud/001-fix-vulakan-and-x11.patch deleted file mode 100644 index 05a24b61a43..00000000000 --- a/package/batocera/utils/mangohud/001-fix-vulakan-and-x11.patch +++ /dev/null @@ -1,108 +0,0 @@ -diff --git a/src/meson.build b/src/meson.build -index 03319831c8..3af714a6bb 100644 ---- a/src/meson.build -+++ b/src/meson.build -@@ -146,12 +146,12 @@ if is_unixy - 'loaders/loader_x11.cpp', - 'shared_x11.cpp', - ) -- endif - - opengl_files += files( - 'loaders/loader_glx.cpp', - 'gl/inject_glx.cpp', - ) -+ endif - - if get_option('with_wayland').enabled() - pre_args += '-DHAVE_WAYLAND' -@@ -175,6 +175,7 @@ link_args = cc.get_supported_link_arguments(['-Wl,-Bsymbolic-functions', '-Wl,-z - # meson fails to check version-script so just force add - link_args += '-Wl,--version-script,@0@'.format(join_paths(meson.current_source_dir(), 'mangohud.version')) - -+mangohud_static_lib = [] - if get_option('use_vulkan') - mangohud_static_lib = static_library( - 'MangoHud', -@@ -217,8 +218,8 @@ endif - - mangohud_shared_lib = shared_library( - 'MangoHud', -- objects: mangohud_static_lib.extract_all_objects(), -- link_with: mangohud_static_lib, -+ objects: get_option('use_vulkan') ? mangohud_static_lib.extract_all_objects() : [], -+ link_with: get_option('use_vulkan') ? mangohud_static_lib : [], - link_args : link_args, - install_dir : libdir_mangohud, - install: true -diff --git a/src/hud_elements.h b/src/hud_elements.h -index 5c22fe73b2..fb4c3ecb81 100644 ---- a/src/hud_elements.h -+++ b/src/hud_elements.h -@@ -6,7 +6,9 @@ - #include "timing.hpp" - #include - #include "winesync.h" -+#ifdef HAVE_VULKAN - #include "vulkan/vulkan.h" -+#endif - #include - #include "net.h" - #include "overlay_params.h" -@@ -158,6 +160,7 @@ class HudElements{ - - void TextColored(ImVec4 col, const char *fmt, ...); - -+#ifdef HAVE_VULKAN - std::array presentModes = { - VK_PRESENT_MODE_FIFO_RELAXED_KHR, - VK_PRESENT_MODE_IMMEDIATE_KHR, -@@ -176,15 +179,17 @@ class HudElements{ - }; - - VkPresentModeKHR cur_present_mode; -+#endif - - std::string get_present_mode(){ -+#ifdef HAVE_VULKAN - if (is_vulkan) - return presentModeMap[cur_present_mode]; -+#endif - // TODO: the opengl side is probably not as solid. - // But it also might not be possible to figure out if vsync - // is on or off unless we specify it. -- else -- return params->gl_vsync == 0 ? "OFF" : "ON"; -+ return params->gl_vsync == 0 ? "OFF" : "ON"; - - } - }; -diff --git a/src/gl/gl_hud.cpp b/src/gl/gl_hud.cpp -index 83b962d6fc..05486c57b0 100644 ---- a/src/gl/gl_hud.cpp -+++ b/src/gl/gl_hud.cpp -@@ -18,11 +18,12 @@ - - #include - -- -+#ifdef HAVE_X11 - #define GLX_RENDERER_VENDOR_ID_MESA 0x8183 - #define GLX_RENDERER_DEVICE_ID_MESA 0x8184 - - bool glx_mesa_queryInteger(int attrib, unsigned int *value); -+#endif - - namespace MangoHud { namespace GL { - -@@ -148,8 +149,10 @@ void imgui_create(void *ctx, const gl_wsi plat) - HUDElements.vendorID = vendorID; - - uint32_t device_id = 0; -+#ifdef HAVE_X11 - if (plat == gl_wsi::GL_WSI_GLX) - glx_mesa_queryInteger(GLX_RENDERER_DEVICE_ID_MESA, &device_id); -+#endif - - SPDLOG_DEBUG("GL device id: {:04X}", device_id); - init_gpu_stats(vendorID, device_id, params); diff --git a/tools/frametime-bench/README.md b/tools/frametime-bench/README.md new file mode 100644 index 00000000000..ef05060af40 --- /dev/null +++ b/tools/frametime-bench/README.md @@ -0,0 +1,71 @@ +# Frametime-based fps benchmarking (Track C) + +Replaces eyeballed on-screen fps counters with real frametime logging + the +industry-standard Avg FPS / 1% Low metrics. Deliberately avoids generic +hooking (mangohud-style EGL interposers previously crashed this board's +blob driver) - each logger is a small source patch at a single, specific +per-frame entry point. + +## Hook points (4 total) + +| # | Location | Covers | +|---|----------|--------| +| 1 | `SDL_EGL_SwapBuffers()` in sdl2 (`board/batocera/patches/sdl2/sdl2_frametime_log.patch`) | standalone SDL2 apps: flycast, mupen64plus, PPSSPP standalone | +| 2 | `video_driver_frame()` in retroarch (`package/batocera/emulators/retroarch/retroarch/900-frametime-log.patch`) | all libretro cores (GLES/GL) | +| 3 | `wsi_layer_vkQueuePresentKHR()` in vulkan-wsi-layer (`package/batocera/gpu/vulkan/vulkan-wsi-layer/0003-frametime-log.patch`) | every Vulkan present on this board (standalone or libretro), incl. AetherSX2 (config-forced Vulkan, no source patch needed) | +| 4 | `ScreenPanelGL::drawScreenGL()` swap in melonds (`package/batocera/emulators/melonds/005-frametime-log.patch`) | melonDS OpenGL Classic renderer (Qt-based, not covered by #1 or #3) | + +Not covered, and not fixable the same way: +- **drastic** - closed-source binary. NDS is already concluded (drastic + 90-100%, melonDS the only dual-screen option) - use its built-in Show + Speed OSD if a number is ever needed. +- **Dolphin standalone** - use its own built-in + `Graphics > Advanced > "Log Render Time to File"` instead (per-frame ms + file, read with `--ms` below). Renderer-agnostic, no patch needed. +- **pcsx2-qt (non-AetherSX2 PS2 core)** - already out of the matrix due to + the GPU DATA_INVALID_FAULT issue; not worth instrumenting. + +Loggers are no-ops (single env-var check) when `FRAMETIME_LOG` / +`FRAMETIME_LOG_VK` aren't set, so they can stay in every Track C image +permanently. + +## Env vars + +- `FRAMETIME_LOG=` - hooks 1, 2, 4 (SDL2 / RetroArch / melonDS) +- `FRAMETIME_LOG_VK=` - hook 3 (vulkan-wsi-layer) + +Use **both** at once for RetroArch-with-Vulkan cores (2 and 3 both fire); +treat the wsi-layer file as the source of truth since it captures the +actual present, matching PresentMon/standard-tool semantics. + +## Running a measurement + +``` +FRAMETIME_LOG=/userdata/ft-C-psp-tekken6-run1.csv emulatorlauncher ... +``` + +(same SSH manual-launch pattern already used for the fps matrix - pull +`WAYLAND_DISPLAY`/`XDG_RUNTIME_DIR`/`LANG` from the live ES/labwc process +env before launching.) + +3-5 minutes per run. Start from the same savestate/point across A/B/C. +Keep VU8M off (HDMI only) unless the dual-screen case itself is what's +being measured. + +## Analysis + +``` +scp root@:/userdata/ft-C-psp-tekken6-run1.csv . +python3 tools/frametime-bench/ft-stats.py ft-C-psp-tekken6-run1.csv +``` + +Dolphin's built-in log: + +``` +python3 tools/frametime-bench/ft-stats.py dolphin_render_times.txt --ms +``` + +Output: frame count, Avg FPS, 1% Low, p99, 0.1% Low (reference only - a +3-5 min run only has ~10-20 samples in the slowest 0.1%). For RetroArch +2-column logs, a "코어 실제 렌더 fps" line is also printed, separating +out duped/no-new-frame calls from genuine core-rendered frames. diff --git a/tools/frametime-bench/ft-stats.py b/tools/frametime-bench/ft-stats.py new file mode 100644 index 00000000000..16d54039f35 --- /dev/null +++ b/tools/frametime-bench/ft-stats.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""usage: ft-stats.py [--ms] [--warmup 60] + +Computes Avg FPS and 1% Low from a frametime log produced by the +FRAMETIME_LOG / FRAMETIME_LOG_VK loggers patched into sdl2, retroarch, +vulkan-wsi-layer, and melonds on this board's Track C image. + + csv 1 column = usec timestamps (SDL2 / vulkan-wsi-layer loggers) + 2 columns = usec timestamp, dupe flag (RetroArch logger) + --ms Dolphin's built-in "Log Render Time to File" mode: each row is a + per-frame duration in milliseconds instead of a timestamp. + +All fps figures are computed in the frametime domain (mean of frametimes, +then inverted) rather than by averaging instantaneous fps values, to avoid +harmonic-mean distortion. +""" +import sys +import argparse +import numpy as np + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("csv") + ap.add_argument("--ms", action="store_true", help="Dolphin per-frame ms log") + ap.add_argument("--warmup", type=float, default=60.0, + help="seconds to discard from the start (shader/pipeline warmup)") + args = ap.parse_args() + + raw = np.genfromtxt(args.csv, delimiter=",") + if raw.ndim == 1: + raw = raw[:, None] + + if args.ms: + # Dolphin: each row = frametime in ms + ft = raw[:, 0] / 1000.0 + t = np.cumsum(ft) + else: + # usec timestamps; frametime = diff between consecutive presents + t = raw[:, 0] / 1e6 + ft = np.diff(t) + t = t[1:] - t[0] + + keep = t > args.warmup + ft = ft[keep] + n = len(ft) + if n < 2: + sys.exit(f"error: only {n} frames after warmup cutoff ({args.warmup}s) - log too short or bad file") + + if raw.shape[1] > 1 and not args.ms: + # RetroArch dupe-flag column: separate "real" (non-duped) render rate + real = raw[1:, 1][keep].astype(bool) + print(f"코어 실제 렌더 fps : {real.sum() / ft.sum():6.1f} (dupe 제외)") + + s = np.sort(ft)[::-1] # slowest first + low1 = s[:max(1, n // 100)] + low01 = s[:max(1, n // 1000)] + + print(f"프레임 수 : {n} ({ft.sum():.0f}s)") + print(f"Avg FPS : {n / ft.sum():6.1f}") + print(f"1% Low : {1 / low1.mean():6.1f}") + print(f"p99 : {1 / np.percentile(ft, 99):6.1f}") + print(f"0.1% Low (참고) : {1 / low01.mean():6.1f}") + + +if __name__ == "__main__": + main()