Compare commits

...
Author SHA1 Message Date
xXJsonDeruloXx ceb0bee79a feat: low-latency controller and touch toggles in the session drawer 2026-10-14 07:58:55 -04:00
The412Banner 4b17bc9a23 feat: rewrite the Epic and Amazon store modules (#802)
Both store modules are rewritten from a protocol-level spec: new Rust engines behind a small JNI surface (nativeCall/nativeInstall) and thin Kotlin facades, with the launcher's own user agent.

Epic: sign-in carries over, library, manifest download with byte-exact verification, assembly, uninstall, launch arguments, achievements.
Amazon: sign-in carries over, library, manifest download with per-file hash checks, path checks, executable detection from fuel.json, uninstall.
On-disk install layout is unchanged, so existing installs keep working.

Store game pages now take controller focus on their lead hero action (Install, Uninstall, Pause, Resume, Get, Buy), fall back to Back, and keep D-pad focus inside the page while it covers the library.
2026-10-14 07:20:06 -04:00
Kurt Himebauch 4ac78621cc docs: credit Phobos for Epic and GOG integration work (#801) 2026-10-14 06:41:42 -04:00
Kurt Himebauch 079261759e perf: the session's background watchers share two loops and one D-Bus service process (#575) 2026-10-14 01:02:24 -04:00
Kurt Himebauch 3fe03c0a88 perf: Steam's web helper runs its GPU, audio and zygote work inside one process (#566) 2026-10-14 01:01:49 -04:00
Kurt Himebauch 6211faf6bc Verify native SD-card library paths before binding them (#757) 2026-10-14 00:59:38 -04:00
Kurt Himebauch 81e645a917 Fix microphone capture in DirectAudio games (#747) 2026-10-14 00:58:52 -04:00
Kurt Himebauch 06b09b37bd Preserve native Linux and FEX compatibility choices (#749) 2026-10-14 00:57:45 -04:00
Kurt Himebauch 0c3444afbc Wait for Steam shortcut overviews before cold launches (#758) 2026-10-14 00:55:04 -04:00
Kurt Himebauch 0988cdc43a Run CodeRabbit reviews independently of APK builds (#777) 2026-10-14 00:29:37 -04:00
Kurt Himebauch 06bd0de4d7 feat: redesign the store game page's facts, description and achievements (#775) 2026-10-13 23:33:43 -04:00
Kurt Himebauch 3a408058ba Configure CodeRabbit reviews (#774) 2026-10-13 22:47:19 -04:00
Juan MartíandThe412Banner a074ba994b Improve store install storage checks and recovery; adaptive parallel Epic installs (#710)
Space checks before Epic, GOG and Amazon installs (measured zero vs unknown), clear failure reasons with retry/clear, resume-aware checks, and safe per-file replacement so a failed or cancelled update never breaks an installed game.

Epic installs stream chunks straight into staged files in parallel (most bytes written once, a small budgeted cache for shared chunks, about 1x game size instead of 2x), pace updates by free space, and keep the lookahead, batch and bounded Java writers as hidden alternatives.

Co-authored-by: Juan Marti Mercado <[email protected]>
Co-authored-by: The412Banner <[email protected]>
2026-10-13 22:29:23 -04:00
d310e59fce fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard (#766)
* fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard

Android repeats a held key as more key presses. Those were forwarded to the
session as more wl_keyboard presses, and KWin's xkb counts every press of a
modifier, so Shift held for a few capitals took several presses and one
release and stayed held: digits typed !@#, letters stayed capital. The same
applied to Ctrl and Alt.

- the compositor no longer sends a press for a key that is already down;
  releases always go through (clients repeat keys themselves from the
  repeat_info it already sends)
- the app stops forwarding Android's repeat events for keyboard keys

Reported by whatsupbros with a Keychron Q7 over USB - thank you.

* Remove stale comments from statx patch

---------

Co-authored-by: The412Banner <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-13 22:03:28 -04:00
Kurt Himebauch 0d0465d19e Fix Flatpak installs on older Android kernels (#752) 2026-10-13 21:31:26 -04:00
Kurt Himebauch 83ec5708ed cleanup (#764) 2026-10-13 21:27:42 -04:00
The412Banner 4163b31b02 fix: Windows components: .NET Core / Desktop runtimes install again (#740)
droiddeck-msi-install: a DuplicateFile entry onto the file itself (dotnet.exe in the .NET host MSI) is skipped like Windows Installer does, instead of stopping the whole install with SameFileError. Fixes .NET Desktop 3/6/7/8, .NET Core 3 and .NET 5. Thanks to whatsupbros for finding and tracing it.
2026-10-13 19:03:38 -04:00
Kurt Himebauch 02888834d4 fix: pace gamescope's fixed-refresh frame limiter per window by time (#753) 2026-10-13 22:21:01 +00:00
Kirk ec537421ac fix: the Debugging tools hint points to Setup > Diagnostics (#725) 2026-10-13 15:01:40 -04:00
Kirk 5e23c668f3 fix: the Android keyboard opens on the first tap after the system dismissed it (#665) 2026-10-13 14:17:31 -04:00
Jagger15 a12ac8134a Add experimental Quest 2 and Adreno 6xx display-driver support (#676) 2026-10-13 14:16:20 -04:00
0x1Nawaf 4562d61139 Feat/reset all settings (#696) 2026-10-13 13:36:31 -04:00
MaxsTechReview cc5ae4ff0b Add translations for 23 more languages and fill missing strings (#669)
* Add translations for 23 more languages and fill missing strings

* Translate the strings added in main into every language

Cardboard VR and the session drawer's component downloads in all 31
languages, the game details facts and HowLongToBeat labels in the 23 new
ones, and the reworded Components page note there.
2026-10-13 13:34:00 -04:00
MaxsTechReviewandxXJsonDeruloXx f0c52c273b Add opt-in zRAM compression for the Steam client while a game runs (#442)
* Add opt-in zRAM compression for the Steam client while a game runs

A "zRAM compression" toggle under Steam > Settings > Session, below
"Keep downloads running". Off by default, kept once set, and disabled
on devices without swap or with a kernel older than 5.4.

When on, the session watches for Steam's reaper. 20 s into a game the
client and its web helpers page their private anonymous memory out to
the device's zram with MADV_PAGEOUT from inside their own process
(libblsession zram.c, SIGURG), again every 5 minutes, and read every
swapped page back once the game exits. No root is needed: only a
process itself may do this without CAP_SYS_NICE. With the toggle off
nothing is installed or started.

RedMagic NP06J, Palworld: Steam's resident memory fell from ~1.7 GB to
~1.1 GB with ~560 MB in zram, available memory during play rose by
~0.5-0.8 GB, and ~2 MB was left in zram after the read-back.

* Turn zRAM compression on by default below 12 GB of RAM

Until the toggle is changed, it follows the device: on when the kernel
supports it and the device has less than 12 GB of RAM, read from the
advertised memory size on Android 14+ and from total memory before that.
Turning it off is kept.

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-13 13:33:19 -04:00
Kurt Himebauch 975c76e3e8 fix: don't count a re-attached unchanged buffer as a new game frame (#731) 2026-10-13 13:24:50 -04:00
Phobos665 a394dfd31b feat(): Rich Game details for GOG & Epic - Publisher, Developer, Release Date, HLTB & Metacritic (#712) 2026-10-13 11:29:03 -04:00
Kurt Himebauch a2e555f4cc Use preview-builds for the Discord channel name (#729) 2026-10-13 11:27:39 -04:00
Kurt Himebauch 31d12a9f6c Align Discord build names with the app (#726) 2026-10-13 11:17:41 -04:00
Kurt Himebauch 3820445561 feat: Cardboard VR mode with ARM64 SteamVR (#704)
* feat: add Cardboard mode with side-by-side lens-distorted eyes and 3DoF head tracking

* fix: derive the Cardboard field of view from the panel and recentre fully when far from upright

* feat: share the Cardboard head pose with the guest and add a SteamVR driver for it

* feat: run SteamVR's compositor on Turnip/KGSL with a display-extension layer and a driver-direct HMD

* feat: show SteamVR frames in Cardboard mode through a driver-side presenter and a fake direct-mode display

* feat: install and start SteamVR for Cardboard mode from the session drawer

* docs: describe Cardboard mode and the SteamVR path

* feat: drive SteamVR hands from the game pad and keep the Cardboard headset out of standby

* docs: record where the SteamVR dashboard stops

* fix: map Cardboard head tracking to the screen's axes and keep turns about the real vertical as yaw

* fix: give Cardboard its previous picture back when SteamVR stops

* fix: report sem_otime from emulated SysV semaphores so SteamVR's web helper can start

* fix: bridge SteamVR's shared semaphores across processes and keep Steam alive while SteamVR runs

* feat: present SteamVR straight to DroidDeck's compositor at panel resolution

* docs: update Cardboard VR notes for the dashboard, semaphore bridge and direct presenter

* fix: keep Wine and FEX signal handlers in the VR layer; add semaphore tracing

* fix: clear a stale SteamVR active marker on status

* fix: clear only an old SteamVR active marker

* fix: unblock SteamVR clients on Turnip and keep Steam's web helper alive across SteamVR runs

* fix: judge shared waits before a submit's own signals and on Vulkan 1.0 devices

* docs: record VR games, SteamVR Home and OpenXR results on the Thor

* docs: add Cardboard VR learnings from the Thor bring-up

* docs: add Cardboard VR screenshots from the Thor

* fix: keep the VR layer's display stand-in to SteamVR runs and clear a leftover marker at session start

* fix: report SteamVR start and stop failures from the helper's exit code
2026-10-13 14:36:56 +00:00
Kurt Himebauch 75d7edd2c3 fix: anchor wp_presentation timestamps to the vsync tick (#717) 2026-10-13 13:29:32 +00:00
Kurt Himebauch da8fe068d9 fix: cap the session refresh rate at the device's peak refresh rate (#715) 2026-10-13 13:16:29 +00:00
Kurt Himebauch 8f9ce19638 Fix FEX dropdown scaling and Discover search focus (#681) 2026-10-13 07:01:05 -04:00
Kurt Himebauch b74edb7e8d Download components from the session drawer (#679) 2026-10-12 22:18:47 -04:00
978 changed files with 88596 additions and 47824 deletions

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
language: en-US
early_access: false
tone_instructions: >-
Prioritize actionable correctness, security, and runtime regressions. Keep
feedback concise. Do not request explanatory code comments; preserve license
headers and compiler-consumed annotations.
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: true
high_level_summary_in_walkthrough: true
poem: false
sequence_diagrams: false
tools:
github-checks:
enabled: false
auto_review:
enabled: true
auto_incremental_review: true
auto_pause_after_reviewed_commits: 0
drafts: false
pre_merge_checks:
docstrings:
mode: "off"
title:
mode: "off"
description:
mode: "off"
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: false
simplify:
enabled: false
autofix:
enabled: false
fix_ci:
enabled: false
resolve_merge_conflict:
enabled: false
path_instructions:
- path: "**/*"
instructions: >-
For cleanup changes, compare executable behavior against the PR merge
base. Check for removed guards, changed return values, weakened compiler
flags, and deleted regression tests. A passing APK build alone does not
establish behavior preservation. Avoid style-only or comment-coverage
requests.
- path: "app/src/main/**"
instructions: >-
Review Android lifecycle, external-display visibility, input cancellation,
permissions, threading, and Compose state. For JNI, native, and Rust
changes, check ABI compatibility, resource ownership, synchronization,
and error paths. Preserve persisted user settings and existing runtime
contracts.
- path: "tools/**"
instructions: >-
Check shell quoting, error handling, path and filesystem semantics, and
Python helper behavior. Preserve MSI same-file protection and strict
compiler checks. Verify downstream patches apply to their pinned sources
and retain intended executable behavior, including embedded scripts.
- path: ".github/workflows/**"
instructions: >-
Check job permissions, triggers, merge validation, build variants,
signing, artifact paths, and release conditions. Verify checks apply to
the current PR head and keep review configuration advisory.
-1
View File
@@ -1,3 +1,2 @@
# Vendored files preserve upstream license spacing and unified-diff context lines.
tools/comet/LICENSE whitespace=-blank-at-eof
tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof
-2
View File
@@ -1,7 +1,5 @@
name: Build AAudio sink module
# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the
# audio bundle. Runs on pushes that touch the module.
on:
push:
paths:
-9
View File
@@ -1,13 +1,5 @@
name: Build desktop package (KDE Plasma)
# The desktop the app installs into its Linux runtime on first use: KDE Plasma as SteamOS's desktop
# mode has it, the closure of tools/desktop-kde/seeds.txt over Arch Linux ARM minus what the runtime
# already carries, plus tools/desktop-kde/overlay (tools/desktop-kde/build.sh), every package
# signature-checked. Published as a release in Droid-Deck/DroidDeck-Components with DroidDeck's
# other components; the apk pins it in tools/desktop-kde/release.env (tag, sha256, size). A
# published tag is never rebuilt: the packages are Arch Linux ARM's current ones, so a rebuild
# would not match the pinned sha256. A changed package gets a new tag (desktop-kde-r2, ...) and a
# release.env update.
on:
push:
paths:
@@ -72,7 +64,6 @@ jobs:
KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out
cp out/kwin/desktop-kde.kwin.txt out/
echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)"
# The package itself too, briefly: enough to try a build on a device before publishing it.
- uses: actions/upload-artifact@v4
with:
name: desktop-kde
-6
View File
@@ -1,9 +1,5 @@
name: Build gamescope (patched)
# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under
# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a
# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The
# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin.
on:
push:
paths:
@@ -43,6 +39,4 @@ jobs:
run: |
set -euo pipefail
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
# A component the apk fetches by tag, never something users install: it must not
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
+5 -43
View File
@@ -22,8 +22,6 @@ permissions:
pull-requests: write
concurrency:
# Replace older builds only for the same PR. Each push/dispatch gets its own group so one main
# build cannot cancel another before it signs and publishes.
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
@@ -186,7 +184,7 @@ jobs:
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/comet/**', 'tools/linuxfs/debian-image.env') }}
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/comet/**', 'tools/linuxfs/debian-image.env', 'tools/vr/**') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
@@ -211,13 +209,14 @@ jobs:
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \
-fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$D/libblaudit.so" tools/proot/fastpath/audit.c
aarch64-linux-gnu-strip --strip-unneeded "$D/libblaudit.so"
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so"
mkdir -p "$D/usr/local/bin"
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \
tools/vr/build.sh --native "$D/usr/local/lib/droiddeck-vr"
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
done
@@ -251,14 +250,12 @@ jobs:
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
done
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c
aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1"
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
rm -rf mango-pkgs
ls -l "$M"
# The Windows components installer's tools (tools/msitools/packages.txt).
T="$D/usr/local/lib/droiddeck-msitools"
mkdir -p "$T" msi-pkgs
grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do
@@ -270,7 +267,6 @@ jobs:
done
install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo"
install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract"
# 7-Zip (the full build with its format plugin) opens installer .exe wrappers.
install -m644 msi-pkgs/usr/lib/7zip/7z "$T/7z"
install -m644 msi-pkgs/usr/lib/7zip/7z.so "$T/7z.so"
for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
@@ -288,10 +284,6 @@ jobs:
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
ls -lR "$D"
# The Debian 11 image the preloads are compiled in comes from a release on this repo
# (published once by ci-debian-image.yml, pinned in tools/linuxfs/debian-image.env): the
# public registry mirrors rate-limit the shared runner addresses and failed builds on a
# busy evening. Only if that download fails is the image pulled, with retries.
- name: Fetch the Debian image the preloads build in
if: steps.preloads.outputs.cache-hit != 'true'
env:
@@ -367,9 +359,6 @@ jobs:
set -euo pipefail
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
# DirectAudio - the game driver (one set per Wine audio interface), the Android-side helper and
# the Steam client's sinks - comes from a pinned release of The412Banner/directaudio, checksummed,
# exactly like gamescope; nothing compiled for audio is kept in this repo.
- name: Fetch DirectAudio (tools/directaudio/release.env)
run: tools/directaudio/fetch.sh "$PWD" "$PWD/sink-out"
- name: Build the AAudio sink into the audio bundle
@@ -407,12 +396,6 @@ jobs:
set -euo pipefail
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
# The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's
# buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android
# target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target
# tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate
# on top of the cached dependency objects, and a lockfile change still starts from the
# previous objects (restore-keys).
- name: Rust toolchain for the store engines (aarch64-linux-android)
id: rust
run: |
@@ -471,9 +454,6 @@ jobs:
python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk
- name: Check the store engines' library
# The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on
# the device with an UnsatisfiedLinkError instead of here. The lockfile check is the
# reproducibility guard: cargo must not have had to re-resolve anything.
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
@@ -484,11 +464,6 @@ jobs:
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Store engines host unit tests
# The crate's #[test] functions are pure host-side logic (manifest parsing, chunk plans,
# assembly from a cache, hashes), so a plain `cargo test` on the runner's own architecture
# runs them; the Android build above compiles none of them. Informational on this first
# run - the point is to learn whether they pass - and bounded so a hang can never hold
# the apk. Make it blocking once it is known green.
timeout-minutes: 10
continue-on-error: true
run: |
@@ -623,12 +598,6 @@ jobs:
variants: ${{ needs.build.outputs.variants }}
secrets: inherit
# Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-<sha>, the
# last KEEP kept), where anyone can download it without a GitHub account, then a post in the
# Discord's builds channel linking to it. The release is made with a token of the org's
# "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the
# DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret.
# DISCORD_BUILDS_WEBHOOK is the channel's webhook URL.
announce:
needs: [build, sign]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
@@ -655,8 +624,6 @@ jobs:
owner: Droid-Deck
repositories: DroidDeck-CI
# What this build is, for the top of the release notes: the merged PR's title and the first
# paragraph of its description, or the commit's subject for a push made without a PR.
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
@@ -706,15 +673,12 @@ jobs:
gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true
gh release create "$tag" -R "$CI_REPO" --target main --prerelease \
--title "main ${GITHUB_SHA::7}" --notes "$notes" signed/*
# Keep the newest main builds by publication time. GitHub App releases can share createdAt.
gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' |
while IFS= read -r old; do
# Never remove this run's release, even if the API returns an unexpected ordering.
if [ "$old" = "$tag" ]; then
continue
fi
# Another main publish may have removed the same old release concurrently.
if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then
if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then
echo "::error::Could not delete old release $old"
@@ -730,7 +694,7 @@ jobs:
echo "apk=$GITHUB_SERVER_URL/$CI_REPO/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
echo "Published $url"
- name: Post to Discord
- name: Post the Preview build to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
@@ -744,8 +708,6 @@ jobs:
fi
commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA")
subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit")
# A merge commit's subject is "Merge pull request #N from ..."; its body's first line is
# the PR's title, which says more.
title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit")
pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject")
if [ -n "$pr" ]; then
@@ -754,7 +716,7 @@ jobs:
what="[\`${GITHUB_SHA::7}\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA): $subject"
fi
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
content=$(printf '**New signed build** `main-%s`\n%s\n%s' "${GITHUB_SHA::7}" "$what" "$get")
content=$(printf '**New signed Preview build** `main-%s`\n%s\nIn the app: **Updates → Preview**\n%s' "${GITHUB_SHA::7}" "$what" "$get")
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
echo "Posted: $content"
-4
View File
@@ -1,8 +1,5 @@
name: Publish the CI Debian image
# Pulls the Debian 11 image the x86 preloads build in, once, and publishes it as a release asset
# on this repo so build.yml never depends on a registry mirror's rate limit. Run it again (with a
# new tag) only to refresh the image; tools/linuxfs/debian-image.env names the one builds use.
on:
workflow_dispatch:
@@ -32,7 +29,6 @@ jobs:
run: |
set -euo pipefail
image=public.ecr.aws/docker/library/debian:bullseye
# A published image is pinned by checksum in the build; never replace one by accident.
if [ "$REPLACE" != true ] && gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' 2>/dev/null | grep -qx debian-bullseye.tar.zst; then
echo "::error::$TAG already carries debian-bullseye.tar.zst; publish to a new tag, or set replace and re-pin tools/linuxfs/debian-image.env"
exit 1
+4 -5
View File
@@ -1,9 +1,9 @@
name: PR Release Cleanup
run-name: remove PR #${{ github.event.pull_request.number }}'s test build
run-name: remove PR
# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it
# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing
# from the PR is checked out or run.
on:
pull_request_target:
types: [ closed ]
@@ -32,7 +32,6 @@ jobs:
set -euo pipefail
state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .state)
echo "closed=$([ "$state" = closed ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
- uses: actions/create-github-app-token@v2
if: ${{ steps.state.outputs.closed == 'true' }}
id: ci-token
-16
View File
@@ -11,8 +11,6 @@ on:
pull_request_target:
types: [labeled]
branches: [main]
# A PR from a branch of this repo is signed on its own after each green build: only people who
# can push here can change it. A fork's PR waits for a maintainer's publish-test label.
workflow_run:
workflows: [Build APK]
types: [completed]
@@ -53,7 +51,6 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Every variant a main build ships, so a PR build can be tried under any of them.
- name: The packages a signed build ships as
id: variants
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
@@ -68,8 +65,6 @@ jobs:
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
# The automatic path stands down quietly, never failing: a draft, a PR labelled
# no-test-build, or a build that a later push has already overtaken.
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
if [ "$EVENT_NAME" = workflow_run ]; then
echo "automatic=true" >> "$GITHUB_OUTPUT"
@@ -94,9 +89,6 @@ jobs:
head=$(jq -r '.head.sha' <<<"$pr")
base=$(jq -r '.base.sha' <<<"$pr")
# The label is often added while the PR's build is still running, or just after a push
# before it has started: wait for the latest build of the head commit to finish (a PR
# build takes about 4 minutes) rather than fail and need the label added again.
run_id=
for attempt in $(seq 1 60); do
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
@@ -116,8 +108,6 @@ jobs:
sleep 30
done
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
# GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is
# tied to this PR by its head repository, branch and commit instead.
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
@@ -127,8 +117,6 @@ jobs:
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
# A PR build is made from the PR merged onto main, so it must install over main's own
# build. main is what this job checked out; no release has to exist or be marked latest.
main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle)
[[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; }
mkdir -p "$RUNNER_TEMP/pr"
@@ -188,8 +176,6 @@ jobs:
pattern: droiddeck-pr-signed-*
merge-multiple: true
path: signed
# What this build is, for the top of the release notes: the PR's title and the first
# paragraph of its description.
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
@@ -208,7 +194,6 @@ jobs:
fi
} > notes-head.md
cat notes-head.md
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
- uses: actions/create-github-app-token@v2
id: ci-token
with:
@@ -250,7 +235,6 @@ jobs:
echo "first=$first" >> "$GITHUB_OUTPUT"
gh release upload "$tag" signed/* -R "$repo" --clobber
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
# Drop the previous build's files: anything this build did not just upload.
keep=$(cd signed && ls | jq -R . | jq -sc .)
gh api "repos/$repo/releases/tags/$tag" |
jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' |
-3
View File
@@ -63,7 +63,6 @@ jobs:
artifact: droiddeck-release
secrets: inherit
# The release is made here, from the apks this run signed, so it traces back to its Build APK run.
publish:
name: publish the release
needs: [find, sign]
@@ -94,7 +93,6 @@ jobs:
if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::release $VERSION already exists"; exit 1
fi
# The notes' first line is the title; media paths become raw URLs at this release's tag.
title=$(sed -n '1s/^# *//p' "$notes")
[ -n "$title" ] || title="DroidDeck $VERSION"
raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/"
@@ -110,7 +108,6 @@ jobs:
--target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest
echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY"
# A release made with this run's token fires no release event, so the in-app catalog is refreshed here.
catalog:
needs: publish
uses: ./.github/workflows/update-catalog.yml
-1
View File
@@ -20,7 +20,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
# Always use the current trusted generator, even when a release event points at an older tag.
- uses: actions/checkout@v4
with:
ref: main
+1 -8
View File
@@ -8,24 +8,17 @@ captures/
.externalNativeBuild/
.cxx/
.kotlin/
# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two
# copies cannot drift.
app/src/main/assets/linuxfs/
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/.proot-inputs
app/src/main/jniLibs/arm64-v8a/.proot-outputs
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into
# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source.
app/src/main/rust/stores/target/
app/src/main/assets/droiddeck-esync/
# Kotlin compiler session files (build-tool state, never source).
.kotlin/
# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see
# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey.
*.jks
*.keystore
*.p12
@@ -37,6 +30,6 @@ droiddeck-release-key/
password.txt
__pycache__/
# Local signing with the release key (tools/build_local.sh); holds a password.
.signing.env
app/src/main/assets/directaudio/
/tools/vr/out/
+1 -1
View File
@@ -37,4 +37,4 @@ Compatibility and performance vary by device; hardware validation is limited. De
## Credits and licence
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
-63
View File
@@ -49,19 +49,12 @@ if (!(treeState in ['clean', 'dirty'])) {
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
// What the in-app updater compares against the published builds: the commit this build is of (a PR
// build's head, not the merge CI makes of it), its PR, and whether CI made it at all.
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
// The release key's certificate: a build signed with it, from CI or a computer, updates in place.
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds.
def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' }
// The desktop package this apk installs (tools/desktop-kde/release.env): published once per tag in
// DroidDeck-Components, pinned here with its checksum like gamescope and the esync packs, so the
// desktop scripts in the apk always get the package they were made for.
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
@@ -72,7 +65,6 @@ if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DES
if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) {
throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG)
}
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
.findAll { it.trim() && !it.startsWith('#') }
.collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" }
@@ -81,16 +73,11 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
android {
namespace 'com.droiddeck.launcher'
compileSdk 34
// The NDK is pinned by property so CI can hand in whatever the runner already has rather than
// downloading a second copy; see gradle.properties.
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
defaultConfig {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
// 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a
// file out of its own data directory, and this app's entire job is to exec a rootfs:
// proot, gamescope, the Steam client and everything it launches all live under files/.
targetSdkVersion 28
versionCode 13
versionName "0.4.0"
@@ -125,10 +112,6 @@ android {
}
signingConfigs {
// AOSP testkey ([email protected]), the same key Bannerlator ships with. It is publicly
// known, so it proves nothing about who built the apk - what it buys is ONE stable
// signature: every build installs over the last one instead of asking the user to
// uninstall first. v1 (JAR) + v2 + v3 so every installer and Android version accepts it.
testkey {
storeFile file("${rootDir}/keystore/testkey.p12")
storeType "PKCS12"
@@ -153,24 +136,15 @@ android {
}
lint {
// targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of
// its own data directory, and executing a rootfs is the whole of what this app does. Lint
// fails a release build over it because Google Play will not accept an sdk that old -
// true, and not something this app can trade away. Every other check stays on.
disable 'ExpiredTargetSdkVersion'
}
packagingOptions {
resources {
// Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest.
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
}
jniLibs {
// The PulseAudio daemon is executed from the native library directory and its
// libraries are read out of the apk by the class loader: both want the old,
// uncompressed-and-extracted layout.
useLegacyPackaging = true
// Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy.
pickFirsts += ['**/libtermux.so']
}
}
@@ -181,9 +155,7 @@ android {
}
testOptions {
// Plain JVM tests: a stray android.util.Log call returns a default instead of throwing.
unitTests.returnDefaultValues = true
// Robolectric tests start activities whose text is now all string resources.
unitTests.includeAndroidResources = true
}
@@ -196,8 +168,6 @@ android {
}
}
// Session scripts must follow this checkout even when the native asset bundle is cached.
// A stale local bundle can omit a newly required helper on a fresh runtime install.
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
from(rootProject.file('tools/linuxfs/overlay')) {
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
@@ -206,8 +176,6 @@ def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
}
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
// Prepared audio lives under build/, so native builds never rewrite the tracked base bundle.
// Copying into one asset root also prevents an old base bundle winning a duplicate asset merge.
def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
@@ -218,12 +186,7 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
// Unit tests and previews remain usable without Docker. Packaging an APK requires the complete
// runtime payload prepared by the local helper or CI, with matching sources and output hashes.
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
// The session scripts are re-copied into the asset tree by stageSessionScripts; with parallel
// task execution this check once hashed a file mid-copy and failed a main build. Hash only
// once the staging has finished.
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
workingDir rootProject.projectDir
commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify'
@@ -232,14 +195,6 @@ tasks.configureEach { task ->
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
}
// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from
// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build
// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule
// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as
// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI
// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins.
// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores
// then report themselves unavailable instead of the build failing.
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean()
@@ -255,9 +210,6 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
}.standardOutput.asText)
outputs.dir(rustJniLibsDir)
workingDir rustCrateDir
// The configuration cache serialises the task, so everything it needs is resolved here, not
// in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle
// does not always have it on PATH.
def cargoBin = "${System.getProperty('user.home')}/.cargo/bin"
environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}"
environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath
@@ -265,47 +217,32 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
if (!skipRust) environment 'RUSTC', providers.exec {
commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc'
}.standardOutput.asText.get().trim()
// --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code.
// Homebrew's cargo/rustc can precede rustup on macOS and use a different sysroot, despite
// rustup reporting the Android target installed. Run both through the selected toolchain.
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
}
// Skipping Rust must omit its library even if another build left a cached .so behind.
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
dependencies {
// The runtime tarball is a zstd-compressed tar.
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
// Bannerlator's Windows components come as .tar.xz (WinComponents).
implementation 'org.tukaani:xz:1.10'
// The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the
// on-screen pad are Views underneath it, which is what they are in every Compose app.
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
// @Preview for the composables; the tooling that renders them is a debug-only dependency.
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.activity:activity-compose:1.8.2'
// The File Manager, Bannerlator's: its icon set, image thumbnails and the coroutines it
// copies on. Same versions as Bannerlator so the screen stays a straight copy.
implementation 'androidx.compose.material:material-icons-extended'
implementation 'io.coil-kt:coil-compose:2.6.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from
// Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command.
implementation 'com.flyfishxu:kadb-android:1.2.1'
// Android terminal view/emulator used by the in-session secondary-display Linux shell.
implementation 'com.termux.termux-app:terminal-view:0.118.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
testImplementation 'androidx.compose.ui:ui-test-junit4'
// The Android archive does not include the host natives used by JVM archive tests.
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar'
}
-45
View File
@@ -4,34 +4,19 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Session logs go to Downloads so a failed run can be handed over as one folder. -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Store download progress and "installed" / "failed" notices (Android 13+). -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- The session keeps running while the app is not on screen: a foreground service holds the
process at perceptible priority, and a high-performance WiFi lock keeps a backgrounded
download from being throttled to nothing by the radio's power-save. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Optional Wi-Fi discovery: requested only from Steam settings after an explanation. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Ends background processes of another app that holds the Steam login. GameHub starts
itself with the phone via boot receivers and signs this client out seconds after every
login; a normal permission, and it cannot touch anything in the foreground. -->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<!-- The microphone for the Linux session, only when the user turns it on: the DirectAudio
relay helper opens the input stream under this uid, and PulseAudio exposes it to the
Steam client as DirectAudioMic. -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Installs DroidDeck's own updates (Updates page) over itself, after the user allows it. -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- A paired Steam Controller, read over Bluetooth LE out of lizard mode (SteamControllerBle).
Granted at install below targetSdk 31; no scan, so no location. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
@@ -50,11 +35,6 @@
android:appCategory="game"
android:isGame="true">
<!-- A game, to the OS: Android 12+ routes the category through GameManager and the OEM
performance frameworks (Game Booster, Game Turbo, Game Space) key off it. Without it
a regular flagship runs the session under its "app" power profile - a conservative
governor and GPU DVFS - which is why the same chipset feels slower here than in a
gaming phone whose mode is on for everything. The config says what the OS may do. -->
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
<activity
@@ -82,10 +62,6 @@
</intent-filter>
</activity>
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
Home launch either back into a running Steam session or to the regular front end.
Off until the user turns on "Use as a Home screen" (Setup): while it is disabled the
app is not a Home app at all - not offered, not in the "choose Home" list. -->
<activity
android:name=".HomeActivity"
android:enabled="false"
@@ -102,16 +78,7 @@
</intent-filter>
</activity>
<!--
A session keeps a Surface, a compositor thread and a proot tree alive; a configuration
change must not tear that down and rebuild it, so the activity handles them itself.
Landscape only: gamescope is told one output size when it starts.
Resizable, deliberately: a foldable opening or closing changes the window, and the
compositor re-fits the picture with bars. Declared non-resizable, the system keeps a
fixed-shape window instead and STRETCHES it onto the other panel - the app never sees a
size change, so nothing it does can help.
-->
<activity
android:name=".input.ControllerEditorActivity"
android:exported="false"
@@ -132,7 +99,6 @@
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<!-- A store's own sign-in page (GOG, Epic Games, Amazon Games) in a WebView. -->
<activity
android:name=".stores.StoreLoginActivity"
android:exported="false"
@@ -158,29 +124,20 @@
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<!--
Owns the session - gamescope, the Steam client, audio - so it survives the activity
going away. Without it Android demotes the process as soon as the last visible
activity goes, and the low-memory killer reaps the guest: leaving Big Picture to
answer a message would come back to a dead Steam and a half-finished download.
-->
<service
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- Holds the process in the foreground while the Linux runtime downloads and unpacks. -->
<service
android:name=".runtime.RuntimeInstallService"
android:exported="false" />
<service
android:name=".core.WirelessAdbPairingService"
android:exported="false" />
<!-- Keeps the process alive while a store (GOG, Epic, Amazon) game downloads. -->
<service
android:name=".stores.download.StoreDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
@@ -190,8 +147,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_share_paths" />
</provider>
<!-- The app's data folder in Android's file picker ("Open from" > DroidDeck). Only the
system's documents UI may bind it; apps get a file only when the user picks it. -->
<provider
android:name=".files.AppFilesProvider"
android:authorities="${applicationId}.documents"
+1 -16
View File
@@ -1,27 +1,16 @@
# The app's native side: the Wayland compositor the session presents into, the adrenotools loader
# it opens Turnip with, and one small library for the fake-input ring's memory fence.
cmake_minimum_required(VERSION 3.22.1)
project(droiddeck C CXX)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -Wno-unused-function -Wimplicit-function-declaration")
# Every library the app builds is linked with 16 KB-aligned LOAD segments. A 16 KB-page kernel
# (Android 15+ devices) refuses to load a 4 KB-only ELF; a 4 KB kernel loads a 16 KB-aligned one
# unchanged (the alignment is a multiple of its page), so this costs nothing on today's devices
# but a little padding in the file. Prebuilt libraries under jniLibs/ carry their own alignment.
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
add_subdirectory(adrenotools)
# DXVK's DXBC->SPIR-V translator (zlib licence), vendored under thirdparty/dxbc from the same
# subset upstream lsfg-vk uses. Native LSFG frame generation needs it: the Lossless Scaling
# shader chain is DXBC in every build downloadable from Steam.
# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc).
add_subdirectory(thirdparty/dxbc)
# The Win-FG chain self-identifies by two halves: the upstream tag it was copied from (hand-set,
# UPDATE IT when re-copying) and a hash over the sources themselves, computed here, which cannot
# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in.
set(WINFG_UPSTREAM "v0.3.0")
set(WINFG_CHAIN_SRC
framegen/winfg/framegen.cpp
@@ -44,11 +33,7 @@ add_subdirectory(waylandcomp)
add_library(sdnative SHARED sdnative/sdnative.c)
target_link_libraries(sdnative log)
# The device info window's Vulkan section: the phone's own driver, asked once through the system
# loader (opened with dlopen, so a phone without Vulkan still loads the app).
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
target_link_libraries(deviceinfo dl)
# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its
# prebuilt libtermux.so with a redundant libstdc++.so dependency.
add_library(termux SHARED termux/termux.c)
@@ -3,7 +3,6 @@
aarch64-linux-gnu-gcc -c src/bcenabler_patch.s -o /tmp/patch.o
aarch64-linux-gnu-objcopy -O binary -j .text /tmp/patch.o /tmp/patch.bin
# Must be smaller than a single page
bin2c -o gen/bcenabler_patch.h -n PatchRawData /tmp/patch.bin
rm /tmp/patch.o
@@ -1,6 +1,4 @@
/* Generated by bin2c, do not edit manually */
/* Contents of file /tmp/patch.bin */
const long int PatchRawData_size = 264;
const unsigned char PatchRawData[264] = {
0x1F, 0x0C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x08, 0x80, 0x52, 0x3E, 0x00, 0x00, 0x14,
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -11,29 +12,14 @@ extern "C" {
#include <stdint.h>
/**
* @brief Describes the level of BCeNabler support for a driver
*/
enum adrenotools_bcn_type {
ADRENOTOOLS_BCN_INCOMPATIBLE, //!< Driver isn't supported by BCeNabler
ADRENOTOOLS_BCN_BLOB, //!< Driver already supports BCn textures so BCeNabler isn't necessary
ADRENOTOOLS_BCN_PATCH //!< Driver can be patched with BCeNabler to support BCn textures
ADRENOTOOLS_BCN_INCOMPATIBLE,
ADRENOTOOLS_BCN_BLOB,
ADRENOTOOLS_BCN_PATCH
};
/**
* @brief Checks the status of BCn support in the supplied driver
* @param major The major part of VkPhysicalDeviceProperties::driverVersion
* @param minor The minor part of VkPhysicalDeviceProperties::driverVersion
* @param vendorId VkPhysicalDeviceProperties::vendorId
*/
enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t minor, uint32_t vendorId);
/**
* @brief Patches the Adreno graphics driver to enable support for BCn compressed formats
* @note adrenotools_get_bcn_type MUST be checked to equal ADRENOTOOLS_BCN_PATCH before calling this
* @param vkGetPhysicalDeviceFormatPropertiesFn A pointer to vkGetPhysicalDeviceFormatProperties obtained through vkGetInstanceProcAddr. This is used to find the correct function to patch
* @return If the patching succeeded, if false the driver will be in an undefined state
*/
bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn);
#ifdef __cplusplus
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -10,60 +11,16 @@ extern "C" {
#include <stdbool.h>
#include "priv.h"
/**
* @brief Opens a new libvulkan.so instance according to `flags`
* @remark IMPORTANT: The app MUST be packaged w/ useLegacyPackaging = true. The reason for this is
* that we install the hook in hookLibDir; but this folder won't be set correctly when useLegacyPackaging == false
* because Android expects to read the *.so directly from the apk (instead of decompressing them into nativeLibraryDir).
* @param dlopenMode The dlopen mode to use when opening libvulkan
* @param featureFlags Which adrenotools driver features to enable
* @param tmpLibDir A writable directory to hold patched libraries, only used on api < 29 due to the lack of memfd support. If nullptr is passed and the API version is < 29 memfd usage will be attempted and if unsupported nullptr will be returned
* @param hookLibDir The directory holding the built hooks.
* IMPORTANT: This path MUST point to whatever `getApplicationInfo().nativeLibraryDir` (Kotlin code) returns.
* Failing to do so will result in this function returning a valid ptr but then the hook will fail and either the original driver is loaded as fallback, or vkEnumeratePhysicalDevices returns 0 devices.
* @param customDriverDir The directory to load a custom GPU driver named according to `customDriverName` from. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`.
* IMPORTANT: This path MUST NOT be on sdcard/storage. i.e. use ANativeActivity::internalDataPath and often looks like "/data/user/0/com.example.app/files/".
* Otherwise you will get permission denied because dlopen() can't be done on *.so libraries that any random user or application can manipulate (which would be a security risk).
* @param customDriverName The soname of the custom driver to load. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`
* @param fileRedirectDir The directory which to redirect all file accesses performed by the driver to. Only used if ADRENOTOOLS_DRIVER_FILE_REDIRECT is set in `featureFlags`
* @param userMappingHandle A pointer to a void* which will be set to the mapping handle if ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT is set in `featureFlags`
*/
void *adrenotools_open_libvulkan(int dlopenMode, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle);
/**
* @brief Imports the given CPU mapped memory range into the GSL allocator. This should then be followed by a call to vkAllocateMemory with a matching size which will return a VkDeviceMemory view over the input region
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param hostPtr The host pointer to import
* @param size The size of the region to import
* @return True on success
*/
bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size);
/**
* @brief Maps GPU memory and imports it into the GSL allocator. This should then be followed by a call to adrenotools_mem_cpu_map to map the memory on the CPU, then finally vkAllocateMemory with a matching size
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param size Pointer to a variable containing the size of the region to import, will be updated to contain the required size of the region to allocate CPU side
* @return true on success
*/
bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size);
/**
* @brief Maps the last mapping allocated using adrenotools_mem_gpu_allocate into the given host memory region, such that vkAllocateMemory can then be called
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param hostPtr A pointer to where the mapping should be mapped
* @param size The size of the mapping. MUST be equal to the size returned by adrenotools_mem_gpu_allocate
*/
bool adrenotools_mem_cpu_map(void *handle, void *hostPtr, uint64_t size);
/**
* @note This function should be called after adrenotools_open_libvulkan and Vulkan driver init to check if the mapping import hook loaded successfully
* @return True if the mapping was successfully imported (or initialization succeeded)
*/
bool adrenotools_validate_gpu_mapping(void *handle);
/**
* @brief Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied)
*/
void adrenotools_set_turbo(bool turbo);
#ifdef __cplusplus
@@ -1,13 +1,11 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#include <stdint.h>
/**
* @brief Bitfield enum of additional driver features that can be used with adrenotools_open_libvulkan
*/
enum {
ADRENOTOOLS_DRIVER_CUSTOM = 1 << 0,
ADRENOTOOLS_DRIVER_FILE_REDIRECT = 1 << 1,
@@ -16,12 +14,9 @@ enum {
#define ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC 0xDEADBEEF
/**
* @brief A replacement GPU memory mapping for use with ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT
*/
struct adrenotools_gpu_mapping {
void *host_ptr;
uint64_t gpu_addr; //!< The GPU address of the mapping to import, if mapping import/init succeeds this will be set to ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC
uint64_t gpu_addr;
uint64_t size;
uint64_t flags;
};
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <array>
#include <cstdio>
#include <cstdlib>
@@ -21,7 +22,6 @@ static loader_android_create_namespace_t loader_android_create_namespace;
static bool lib_loaded;
/* Public API */
bool linkernsbypass_load_status() {
return lib_loaded;
}
@@ -55,8 +55,6 @@ android_link_namespaces_all_libs_t android_link_namespaces_all_libs;
android_link_namespaces_t android_link_namespaces;
bool linkernsbypass_link_namespace_to_default_all_libs(android_namespace_t *to) {
// Creating a shared namespace with the default parent will give a copy of the default namespace that we can actually access
// This is needed since there is no way to access a direct handle to the default namespace as it's not exported
static auto defaultNs{android_create_namespace_escape("default_copy", nullptr, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)};
return android_link_namespaces_all_libs(to, defaultNs);
}
@@ -81,7 +79,6 @@ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, android_n
void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, android_namespace_t *ns) {
static std::array<char, PATH_MAX> PathBuf{};
// Used as a unique ID for overwriting soname and creating target lib files
static uint16_t TargetId{};
int libTargetFd{[&] () {
@@ -89,7 +86,6 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li
snprintf(PathBuf.data(), PathBuf.size(), "%s/%d_patched.so", libTargetDir, TargetId);
return open(PathBuf.data(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
} else {
// If memfd isn't supported errno will contain ENOSYS after calling
errno = 0;
int fd{static_cast<int>(syscall(__NR_memfd_create, libPath, 0))};
if (errno == ENOSYS || fd < 0)
@@ -101,21 +97,18 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li
if (libTargetFd == -1)
return nullptr;
// Partially overwrite soname with 3 digits (replacing lib...) with to make sure a cached so isn't loaded
std::array<char, 3> sonameOverwrite{};
snprintf(sonameOverwrite.data(), sonameOverwrite.size(), "%03u", TargetId++);
if (!elf_soname_patch(libPath, libTargetFd, sonameOverwrite.data()))
return nullptr;
// Load our patched library into the hook namespace
android_dlextinfo hookExtInfo{
.flags = ANDROID_DLEXT_USE_NAMESPACE | ANDROID_DLEXT_USE_LIBRARY_FD,
.library_fd = libTargetFd,
.library_namespace = ns
};
// Make a path that looks about right
snprintf(PathBuf.data(), PathBuf.size(), "/proc/self/fd/%d", libTargetFd);
return android_dlopen_ext(PathBuf.data(), flags, &hookExtInfo);
@@ -125,21 +118,19 @@ static void *align_ptr(void *ptr) {
return reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(ptr) & ~(getpagesize() - 1));
}
/* Private */
__attribute__((constructor)) static void resolve_linker_symbols() {
using loader_dlopen_t = void *(*)(const char *, int, const void *);
if (android_get_device_api_level() < 28)
return;
// ARM64 specific function walking to locate the internal dlopen handler
auto loader_dlopen{[]() {
union BranchLinked {
uint32_t raw;
struct {
int32_t offset : 26; //!< 26-bit branch offset
uint8_t sig : 6; //!< 6-bit signature
int32_t offset : 26;
uint8_t sig : 6;
};
bool Verify() {
@@ -148,10 +139,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
};
static_assert(sizeof(BranchLinked) == 4, "BranchLinked is wrong size");
// Some devices ship with --X mapping for exexecutables so work around that
mprotect(align_ptr(reinterpret_cast<void *>(&dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC);
// dlopen is just a wrapper for __loader_dlopen that passes the return address as the third arg hence we can just walk it to find __loader_dlopen
auto blInstr{reinterpret_cast<BranchLinked *>(&dlopen)};
while (!blInstr->Verify())
blInstr++;
@@ -159,10 +148,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
return reinterpret_cast<loader_dlopen_t>(blInstr + blInstr->offset);
}()};
// Protect the loader_dlopen function to remove the BTI attribute (since this is an internal function that isn't intended to be jumped indirectly to)
mprotect(align_ptr(reinterpret_cast<void *>(&loader_dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC);
// Passing dlopen as a caller address tricks the linker into using the internal unrestricted namespace letting us access libraries that are normally forbidden in the classloader namespace imposed on apps
auto ldHandle{loader_dlopen("ld-android.so", RTLD_LAZY, reinterpret_cast<void *>(&dlopen))};
if (!ldHandle)
return;
@@ -187,6 +174,5 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
if (!android_get_exported_namespace)
return;
// Lib is now safe to use
lib_loaded = true;
}
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -9,7 +10,6 @@ extern "C" {
#include <stdint.h>
// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=25;bpv=1;bpt=1
enum {
ANDROID_NAMESPACE_TYPE_REGULAR = 0,
ANDROID_NAMESPACE_TYPE_ISOLATED = 1,
@@ -19,14 +19,8 @@ enum {
ANDROID_NAMESPACE_TYPE_SHARED_ISOLATED = ANDROID_NAMESPACE_TYPE_SHARED | ANDROID_NAMESPACE_TYPE_ISOLATED,
};
/**
* @brief Checks if linkernsbypass loaded successfully and is safe to use
* @note IMPORTANT: This should be called before any calls to the rest of the library are made
* @return true if loading succeeded
*/
bool linkernsbypass_load_status();
// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=86;bpv=1;bpt=1
struct android_namespace_t *android_create_namespace(const char *name,
const char *ld_library_path,
const char *default_library_path,
@@ -41,39 +35,19 @@ struct android_namespace_t *android_create_namespace_escape(const char *name,
const char *permitted_when_isolated_path,
struct android_namespace_t *parent_namespace);
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=3554
typedef struct android_namespace_t *(*android_get_exported_namespace_t)(const char *);
extern android_get_exported_namespace_t android_get_exported_namespace;
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2499
typedef bool (*android_link_namespaces_all_libs_t)(struct android_namespace_t *, struct android_namespace_t *);
extern android_link_namespaces_all_libs_t android_link_namespaces_all_libs;
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2473
typedef bool (*android_link_namespaces_t)(struct android_namespace_t *, struct android_namespace_t *, const char *);
extern android_link_namespaces_t android_link_namespaces;
/**
* @brief Like android_link_namespaces_all_libs but links from the default namespace
*/
bool linkernsbypass_link_namespace_to_default_all_libs(struct android_namespace_t *to);
/**
* @brief Loads a library into a namespace
* @note IMPORTANT: If `filename` is compiled with the '-z global' linker flag and RTLD_GLOBAL is supplied in `flags` the library will be added to the namespace's LD_PRELOAD list
* @param filename The name of the library to load
* @param flags The rtld flags for `filename`
* @param ns The namespace to dlopen into
*/
void *linkernsbypass_namespace_dlopen(const char *filename, int flags, struct android_namespace_t *ns);
/**
* @brief Force loads a unique instance of a library into a namespace
* @param libPath The path to the library to load with hooks applied
* @param libTargetDir A temporary directory to hold the soname patched library at `libPath`, will attempt to use memfd if nullptr
* @param flags The rtld flags for `libName`
* @param ns The namespace to dlopen into
*/
void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, struct android_namespace_t *ns);
#ifdef __cplusplus
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <initializer_list>
#include <cstdint>
#include <errno.h>
@@ -20,7 +21,6 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch
if (ftruncate(targetFd, libStat.st_size) == -1)
return false;
// Map the memory so we can read our elf into it
auto mappedLib{reinterpret_cast<uint8_t *>(mmap(nullptr, libStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, targetFd, 0))};
if (!mappedLib)
return false;
@@ -29,30 +29,25 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch
if (libFd == false)
return false;
// Read lib elf into target file
if (read(libFd, mappedLib, libStat.st_size) != libStat.st_size)
return false;
// No longer needed
close(libFd);
auto eHdr{reinterpret_cast<ElfW(Ehdr) *>(mappedLib)};
auto sHdrEntries{reinterpret_cast<ElfW(Shdr) *>(mappedLib + eHdr->e_shoff)};
// Iterate over section headers to find the .dynamic section
for (ElfW(Half) i{}; i < eHdr->e_shnum; i++) {
auto &sHdr{sHdrEntries[i]};
if (sHdr.sh_type == SHT_DYNAMIC) {
auto strTab{reinterpret_cast<char *>(mappedLib + sHdrEntries[sHdr.sh_link].sh_offset)};
auto dynHdrEntries{reinterpret_cast<ElfW(Dyn) *>(mappedLib + sHdr.sh_offset)};
// Iterate over .dynamic entries to find DT_SONAME
for (ElfW(Xword) k{}; k < (sHdr.sh_size / sHdr.sh_entsize); k++) {
auto &dynHdrEntry{dynHdrEntries[k]};
if (dynHdrEntry.d_tag == DT_SONAME) {
char *soname{strTab + dynHdrEntry.d_un.d_val};
// Partially replace the old soname with the soname patch
size_t charIdx{};
for (; soname[charIdx] != 0 && sonamePatch[charIdx] != 0; charIdx++)
soname[charIdx] = sonamePatch[charIdx];
@@ -1,19 +1,13 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Overwrites a portion of the soname in an elf by loading it into shared memory and modifying .dynstr
* @note IMPORTANT: The supplied soname patch will overwrite the first strlen(sonamePatch) chars of the soname
* @param elfPath Full path to the elf to patch
* @param targetFd FD to use for storing the patched library
* @return True on success
*/
bool elf_soname_patch(const char *elfPath, int targetFd, const char *newSoname);
#ifdef __cplusplus
@@ -11,8 +11,3 @@ linkernsbypass_lib = static_library (
include_directories: ([linkernsbypass_inc, adrenotools_inc]),
dependencies: ([libandroid_dep, libdl_dep])
)
# linkernsbypass_include = static_library('linkernsbypass', sources)
# target_compile_options(['linkernsbypass', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['linkernsbypass', 'android', 'dl'])
# target_include_directories(['linkernsbypass', 'PUBLIC', '.'])
-15
View File
@@ -33,18 +33,3 @@ adrenotools_lib = shared_library(
)
subdir('src/hook')
# target_include_directories(['adrenotools', 'PUBLIC', 'include'])
# target_include_directories(['adrenotools', 'PRIVATE', '.'])
# target_compile_options(['adrenotools', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['adrenotools', 'android', 'linkernsbypass'])
# hooks_deb = subproject('src/hook')
# if build_shared_libs
# target_link_options(['adrenotools', 'PRIVATE', '-Wl,-s,--exclude-libs,liblinkernsbypass.a'])
# endif
# if gen_install_target
# configure_file(['adrenotools.pc.in', 'adrenotools.pc', '@ONLY'])
# install(['FILES', cmake_binary_dir, '/adrenotools.pc', 'DESTINATION', 'lib/pkgconfig'])
# install(['TARGETS', 'adrenotools', 'main_hook', 'file_redirect_hook', 'gsl_alloc_hook', 'hook_impl'])
# install(['DIRECTORY', 'include/', 'DESTINATION', 'include', 'FILES_MATCHING', 'PATTERN', '*.h*'])
# endif
+3 -18
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <fstream>
#include <string>
#include <cstring>
@@ -19,7 +20,6 @@ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t mino
return ADRENOTOOLS_BCN_PATCH;
}
// Searches /proc/self/maps for the first free page after the given address
static void *find_free_page(uintptr_t address) {
std::ifstream procMaps("/proc/self/maps");
@@ -45,62 +45,48 @@ static void *align_ptr(void *ptr) {
bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) {
union Branch {
struct {
int32_t offset : 26; //!< 26-bit branch offset
uint8_t sig : 6; //!< 6-bit signature (0x25 for linked, 0x5 for jump)
int32_t offset : 26;
uint8_t sig : 6;
};
uint32_t raw{};
};
static_assert(sizeof(Branch) == 4, "Branch size is invalid");
// Find the nearest unmapped page where we can place patch code
void *patchPage{find_free_page(reinterpret_cast<uintptr_t>(vkGetPhysicalDeviceFormatPropertiesFn))};
if (!patchPage)
return false;
// Map patch region
void *ptr{mmap(patchPage, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0)};
if (ptr != patchPage)
return false;
// Allow reading from the blob's .text section since some devices enable ---X
// Protect two pages just in case we happen to land on a page boundary
if (mprotect(align_ptr(vkGetPhysicalDeviceFormatPropertiesFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC))
return false;
// First branch in this function is targeted at the function we want to patch
Branch *blInst{reinterpret_cast<Branch *>(vkGetPhysicalDeviceFormatPropertiesFn)};
constexpr uint8_t BranchLinkSignature{0x25};
// Search for first instruction with the BL signature
while (blInst->sig != BranchLinkSignature)
blInst++;
// Internal QGL format conversion function that we need to patch
uint32_t *convFormatFn{reinterpret_cast<uint32_t *>(blInst) + blInst->offset};
// See mprotect call above
// This time we also set PROT_WRITE so we can write our patch to the page
if (mprotect(align_ptr(convFormatFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC))
return false;
// This would normally set the default result to 0 (error) in the format not found case
constexpr uint32_t ClearResultSignature{0x2a1f03e0};
// We replace it with a branch to our own extended if statement which adds in the extra things for BCn
uint32_t *clearResultPtr{convFormatFn};
while (*clearResultPtr != ClearResultSignature)
clearResultPtr++;
// Ensure we don't write out of bounds
if (PatchRawData_size > getpagesize())
return false;
// Copy the patch function to our mapped page
memcpy(patchPage, PatchRawData, PatchRawData_size);
// Fixup the patch code so it correctly returns back to the driver after running
constexpr uint32_t PatchReturnFixupMagic{0xffffffff};
constexpr uint8_t BranchSignature{0x5};
@@ -130,6 +116,5 @@ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) {
asm volatile("ISB");
// Done!
return true;
}
@@ -80,4 +80,4 @@
.L18:
mov w0, 0
.L3:
.word 0xffffffff // Branch fixup
.word 0xffffffff
+3 -13
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <string>
#include <string_view>
#include <sys/stat.h>
@@ -17,17 +18,14 @@
#include <unistd.h>
void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle) {
// Bail out if linkernsbypass failed to load, this probably means we're on api < 28
if (!linkernsbypass_load_status()) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Could not load linkernsbypass\n");
return nullptr;
}
// Use memfd on Q+ since it's guaranteed to work, but only if tmpLibDir wasn't set
if (android_get_device_api_level() >= 29 && !tmpLibDir)
tmpLibDir = nullptr;
// Verify that params for specific features are only passed if they are enabled
if (!(featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) && fileRedirectDir) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_FILE_REDIRECT present but no file redirect folder found\n");
return nullptr;
@@ -43,7 +41,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
return nullptr;
}
// Verify that params for enabled features are correct
struct stat buf{};
if (featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) {
@@ -58,7 +55,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
}
}
// Verify that params for enabled features are correct
if (featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) {
if (!fileRedirectDir) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_REDIRECT_DIR present but no folder parameter was found\n");
@@ -71,22 +67,18 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
}
}
// Create a namespace that can isolate our hook from the classloader namespace
auto hookNs{android_create_namespace("adrenotools-libvulkan", hookLibDir, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)};
// Link it to the default namespace so the hook can use libandroid etc
if (!linkernsbypass_link_namespace_to_default_all_libs(hookNs)) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to hook our namespace to the default one\n");
return nullptr;
}
// Preload the hook implementation, otherwise we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop
auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, hookNs)};
if (!hookImpl) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't preload the hook implementation\n");
return nullptr;
}
// Pass parameters to the hook implementation
auto initHookParam{reinterpret_cast<void (*)(const void *)>(dlsym(hookImpl, "init_hook_param"))};
if (!initHookParam) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't init hook params\n");
@@ -95,7 +87,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
auto importMapping{[&]() -> adrenotools_gpu_mapping * {
if (featureFlags & ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT) {
// This will be leaked, but it's not a big deal since it's only a few bytes
adrenotools_gpu_mapping *mapping{new adrenotools_gpu_mapping{}};
*userMappingHandle = mapping;
return mapping;
@@ -107,7 +98,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
initHookParam(new HookImplParams(featureFlags, tmpLibDir, hookLibDir, customDriverDir, customDriverName, fileRedirectDir, importMapping));
// Load the libvulkan hook into the isolated namespace
if (!linkernsbypass_namespace_dlopen("libmain_hook.so", RTLD_GLOBAL, hookNs)) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to load libvulkan into the isolated namespace\n");
return nullptr;
@@ -150,7 +140,7 @@ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size) {
importMapping->host_ptr = hostPtr;
importMapping->gpu_addr = info.gpuaddr;
importMapping->size = size;
importMapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work
importMapping->flags = 0xc2600;
close(kgslFd);
return true;
@@ -189,7 +179,7 @@ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size) {
mapping->host_ptr = nullptr;
mapping->gpu_addr = info.gpuaddr;
mapping->size = *size;
mapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work
mapping->flags = 0xc2600;
close(kgslFd);
return true;
@@ -15,7 +15,7 @@
#define TAG "hook_impl"
#define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##__VA_ARGS__)
const HookImplParams *hook_params; //!< Bunch of info needed to load/patch the driver
const HookImplParams *hook_params;
int (*gsl_memory_alloc_pure_sym)(uint32_t, uint32_t, void *);
int (*gsl_memory_alloc_pure_64_sym)(uint64_t, uint32_t, void *);
int (*gsl_memory_free_pure_sym)(void *);
@@ -43,7 +43,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: filename: %s", filename);
// Ignore non-vulkan libraries
if (!strstr(filename, "vulkan."))
return android_dlopen_ext(filename, flags, extinfo);
@@ -52,7 +51,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
return fallback();
}
// customDriverDir will be empty if ADRENOTOOLS_DRIVER_CUSTOM isn't set therefore it's fine to have either way
auto driverNs{android_create_namespace(filename, hook_params->customDriverDir.c_str(),
hook_params->hookLibDir.c_str(), ANDROID_NAMESPACE_TYPE_SHARED,
nullptr, extinfo->library_namespace)};
@@ -61,16 +59,12 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
return fallback();
}
// We depend on libandroid which is unlikely to be in the supplied driver namespace so we have to link it over
android_link_namespaces(driverNs, nullptr, "libandroid.so");
// Preload ourself, a new instance will be created since we have different linker ancestory
// If we don't preload we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop
auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, driverNs)};
if (!hookImpl)
return nullptr;
// Pass parameters to ourself
auto initHookParam{reinterpret_cast<void (*)(const void *)>(dlsym(hookImpl, "init_hook_param"))};
if (!initHookParam)
return nullptr;
@@ -86,7 +80,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: applied libfile_redirect_hook");
}
// Use our new namespace to load the vulkan driver
auto newExtinfo{*extinfo};
newExtinfo.library_namespace = driverNs;
@@ -122,8 +115,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: hook failed: failed to apply libgsl_alloc_hook!");
}
// TODO: If there is already an instance of a Vulkan driver loaded hooks won't be applied, this will only be the case for skiavk generally
// To fix this we would need to search /proc/self/maps for the file to a loaded instance of the library in order to read it to patch the soname and load it uniquely
if (hook_params->featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) {
LOGI("hook_android_dlopen_ext: loading custom driver: %s%s", hook_params->customDriverDir.c_str(), hook_params->customDriverName.c_str());
void *handle{android_dlopen_ext(hook_params->customDriverName.c_str(), flags, &newExtinfo)};
@@ -142,7 +133,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
__attribute__((visibility("default"))) void *hook_android_load_sphal_library(const char *filename, int flags) {
LOGI("hook_android_load_sphal_library: filename: %s", filename);
// https://android.googlesource.com/platform/system/core/+/master/libvndksupport/linker.cpp
for (const char *name : {"sphal", "vendor", "default"}) {
if (auto vendorNs{android_get_exported_namespace(name)}) {
android_dlextinfo dlextinfo{
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -1,15 +1,12 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#include <string>
#include <adrenotools/priv.h>
/**
* @brief Holds the parameters needed for all hooks
* @note See comments for adrenotools_open_libvulkan as a reference for member variables
*/
struct HookImplParams {
int featureFlags;
std::string tmpLibDir;
File diff suppressed because it is too large. Load diff
@@ -1,32 +1,12 @@
hook_impl_files = files(['hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h'])
hook_impl_inc = include_directories('../../include')
# hook_impl_lib = shared_library('hook_impl', 'hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h')
# target_compile_options(['hook_impl', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['hook_impl', 'linkernsbypass', 'log'])
# target_include_directories(['hook_impl', 'PRIVATE', '../../include'])
# set_target_properties(['hook_impl', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
main_hook_files = files(['main_hook.c'])
# main_hook_lib = shared_library('main_hook', 'main_hook.c')
# target_compile_options(['main_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['main_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['main_hook', 'hook_impl'])
# set_target_properties(['main_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
file_redirect_hook_files = files(['file_redirect_hook.c'])
# file_redirect_hook_lib = shared_library('file_redirect_hook', 'file_redirect_hook.c')
# target_compile_options(['file_redirect_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['file_redirect_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['file_redirect_hook', 'hook_impl'])
# set_target_properties(['file_redirect_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
gsl_alloc_hook_files = files(['gsl_alloc_hook.c'])
# gsl_alloc_hook_lib = shared_library('gsl_alloc_hook', 'gsl_alloc_hook.c')
# target_compile_options(['gsl_alloc_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['gsl_alloc_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['gsl_alloc_hook', 'hook_impl'])
# set_target_properties(['gsl_alloc_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
hook_impl_lib = shared_library(
'hook_impl',
@@ -5,7 +5,6 @@
thread_local int satisfy_driver_emutls;
// Check libllvm-glnext.so in older drivers for a full list of these, newer ones are slightly more awkward due to the split
enum class OptionType : uint32_t {
ArgsStringList = 1,
QcArgsString = 4,
@@ -21,7 +20,6 @@ struct StringListVal {
struct Option
{
OptionType type;
// PAD
union {
bool boolVal;
int intVal;
@@ -49,7 +47,6 @@ int CompileHook(void *state, OptionsSet *opts) {
Option replacementOpts[ReplacementOptsNum];
uint32_t patchedOptNum{};
// Insert your desired args here - results will be written to stderr
const char *argv[] = {"-help", "-help-hidden", "", NULL };
StringListVal listVal{
.len = 3,
@@ -83,7 +80,6 @@ int CompileHook(void *state, OptionsSet *opts) {
struct LlvmInterface {
uint32_t unk0[3];
// PAD
void *makeContext;
void *delContext;
decltype(CompileHook) *compile;
@@ -30,53 +30,31 @@
#ifndef __QTIGRALLOCDEFS_H__
#define __QTIGRALLOCDEFS_H__
// From gralloc_priv.h
/* Gralloc usage bits indicating the type of allocation that should be used */
/* Refer to BufferUsage in hardware/interfaces/graphics/common/<ver>/types.hal */
/* The bits below are in officially defined vendor space
* i.e bits 28-31 and 48-63*/
/* Non linear, Universal Bandwidth Compression */
#define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC (UINT32_C(1) << 28)
/* Set this for allocating uncached memory (using O_DSYNC),
* cannot be used with noncontiguous heaps */
#define GRALLOC_USAGE_PRIVATE_UNCACHED (UINT32_C(1) << 29)
/* This flag is used to indicate 10 bit format.
* When both GRALLOC_USAGE_PRIVATE_ALLOC_UBWC & GRALLOC_USAGE_PRIVATE_10BIT
* are set then it will indicate UBWC_TP10 format.
* When only GRALLOC_USAGE_PRIVATE_10BIT is set it will indicate linear P010 format.
*/
#define GRALLOC_USAGE_PRIVATE_10BIT (UINT32_C(1) << 30)
/* This flag is used for SECURE display usecase */
#define GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY (UINT32_C(1) << 31)
/* This flag is used to indicate video NV21 format */
#define GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER 1ULL << 48
/* unused legacy flags */
#define GRALLOC_USAGE_PRIVATE_MM_HEAP 0
#define GRALLOC_USAGE_PRIVATE_IOMMU_HEAP 0
/* This flag is set for WFD usecase */
#define GRALLOC_USAGE_PRIVATE_WFD 1ULL << 51
/* This flag is set for HEIF usecase */
#define GRALLOC_USAGE_PRIVATE_HEIF (UINT32_C(1) << 27)
/* TODO(user): Remove when clients stop referencing this flag */
#define GRALLOC_USAGE_PRIVATE_10BIT_TP 0
/* This flag indicates PI format is being used */
#define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI 1ULL << 49
/* This flag is set while CDSP accesses the buffer */
#define GRALLOC_USAGE_PRIVATE_CDSP 1ULL << 50
// OEM specific HAL formats
#define HAL_PIXEL_FORMAT_RGBA_5551 6
#define HAL_PIXEL_FORMAT_RGBA_4444 7
#define HAL_PIXEL_FORMAT_NV12_ENCODEABLE 0x102
@@ -98,7 +76,6 @@
#define HAL_PIXEL_FORMAT_RAW8 0x123
#define HAL_PIXEL_FORMAT_NV12_HEIF 0x116
// 10 bit
#define HAL_PIXEL_FORMAT_ARGB_2101010 0x117
#define HAL_PIXEL_FORMAT_RGBX_1010102 0x118
#define HAL_PIXEL_FORMAT_XRGB_2101010 0x119
@@ -106,7 +83,7 @@
#define HAL_PIXEL_FORMAT_ABGR_2101010 0x11B
#define HAL_PIXEL_FORMAT_BGRX_1010102 0x11C
#define HAL_PIXEL_FORMAT_XBGR_2101010 0x11D
#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 // HAL_PIXEL_FORMAT_YCBCR_P010
#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC 0x124
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS 0x7FA30C0A
@@ -115,7 +92,6 @@
#define HAL_PIXEL_FORMAT_INTERLACE 0x180
// Camera utils format
#define HAL_PIXEL_FORMAT_NV12_LINEAR_FLEX 0x125
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX 0x126
#define HAL_PIXEL_FORMAT_MULTIPLANAR_FLEX 0x127
@@ -123,22 +99,16 @@
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_4_BATCH 0x129
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_8_BATCH 0x130
// FSC Formats
#define HAL_PIXEL_FORMAT_RGB888_UBWC_FSC 0x131
#define HAL_PIXEL_FORMAT_RGB101010_UBWC_FSC 0x132
// v4l2_fourcc('Y', 'U', 'Y', 'L'). 24 bpp YUYV 4:2:2 10 bit per component
#define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT 0x4C595559
// v4l2_fourcc('Y', 'B', 'W', 'C'). 10 bit per component. This compressed
// format reduces the memory access bandwidth
#define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT_COMPRESSED 0x43574259
// UBWC aligned Venus format
#define HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC 0x7FA30C06
#define HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC 0x7FA30C09
// Khronos ASTC formats
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x4_KHR 0x93B1
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x5_KHR 0x93B2
@@ -170,4 +140,4 @@
enum { BUFFER_TYPE_UI = 0, BUFFER_TYPE_VIDEO };
#endif //__QTIGRALLOCDEFS_H__
#endif
@@ -26,7 +26,6 @@
using namespace android::hardware;
using namespace android;
/* Simplified version of hidl::base::V1_0::IBase */
struct IBase : virtual public RefBase {
virtual bool isRemote() const;
@@ -80,8 +80,6 @@ AdrenoMemInfo::AdrenoMemInfo() {
ALOGE(" Failed to load libadreno_utils.so");
}
// Check if the overriding property debug.gralloc.gfx_ubwc_disable
// that disables UBWC allocations for the graphics stack is set
char property[PROPERTY_VALUE_MAX];
property_get(DISABLE_UBWC_PROP, property, "0");
if (!(strncmp(property, "1", PROPERTY_VALUE_MAX)) ||
@@ -123,16 +121,13 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int
break;
}
int raster_mode = 0; // Adreno unknown raster mode.
int padding_threshold = 512; // Threshold for padding surfaces.
// the function below computes aligned width and aligned height
// based on linear or macro tile mode selected.
int raster_mode = 0;
int padding_threshold = 512;
if (LINK_adreno_compute_fmt_aligned_width_and_height) {
// We call into adreno_utils only for RGB formats. So plane_id is 0 and
// num_samples is 1 always. We may have to add uitility function to
// find out these if there is a need to call this API for YUV formats.
LINK_adreno_compute_fmt_aligned_width_and_height(
width, height, 0 /*plane_id*/, GetGpuPixelFormat(format), 1 /*num_samples*/, tile_enabled,
width, height, 0 , GetGpuPixelFormat(format), 1 , tile_enabled,
raster_mode, padding_threshold, reinterpret_cast<int*>(aligned_w),
reinterpret_cast<int*>(aligned_h));
} else if (LINK_adreno_compute_aligned_width_and_height) {
@@ -140,7 +135,7 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int
width, height, bpp, tile_enabled, raster_mode, padding_threshold,
reinterpret_cast<int*>(aligned_w), reinterpret_cast<int*>(aligned_h));
} else if (LINK_adreno_compute_padding) {
int surface_tile_height = 1; // Linear surface
int surface_tile_height = 1;
*aligned_w = UINT(LINK_adreno_compute_padding(width, bpp, surface_tile_height, raster_mode,
padding_threshold));
ALOGW("%s: Warning!! Old GFX API is used to calculate stride", __FUNCTION__);
@@ -157,9 +152,9 @@ void AdrenoMemInfo::AlignCompressedRGB(int width, int height, int format, unsign
unsigned int* aligned_h) {
if (LINK_adreno_compute_compressedfmt_aligned_width_and_height) {
int bytesPerPixel = 0;
int raster_mode = 0; // Adreno unknown raster mode.
int padding_threshold = 512; // Threshold for padding
// surfaces.
int raster_mode = 0;
int padding_threshold = 512;
LINK_adreno_compute_compressedfmt_aligned_width_and_height(
width, height, format, 0, raster_mode, padding_threshold, reinterpret_cast<int*>(aligned_w),
@@ -341,4 +336,4 @@ bool AdrenoMemInfo::IsPISupportedByGPU(int format, uint64_t usage) {
return false;
}
} // namespace gralloc
}
@@ -35,11 +35,10 @@
namespace gralloc {
// Adreno Pixel Formats
typedef enum {
ADRENO_PIXELFORMAT_UNKNOWN = 0,
ADRENO_PIXELFORMAT_R16G16B16A16_FLOAT = 10,
ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24,
ADRENO_PIXELFORMAT_R8G8B8A8 = 28,
ADRENO_PIXELFORMAT_R8G8B8A8_SRGB = 29,
ADRENO_PIXELFORMAT_D32_FLOAT = 40,
@@ -55,140 +54,87 @@ typedef enum {
ADRENO_PIXELFORMAT_P010 = 104,
ADRENO_PIXELFORMAT_YUY2 = 107,
ADRENO_PIXELFORMAT_B4G4R4A4 = 115,
ADRENO_PIXELFORMAT_NV12_EXT = 506, // NV12 with non-std alignment and offsets
ADRENO_PIXELFORMAT_R8G8B8X8 = 507, // GL_RGB8 (Internal)
ADRENO_PIXELFORMAT_R8G8B8 = 508, // GL_RGB8
ADRENO_PIXELFORMAT_A1B5G5R5 = 519, // GL_RGB5_A1
ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, // GL_SRGB8
ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, // GL_SRGB8
ADRENO_PIXELFORMAT_NV12_EXT = 506,
ADRENO_PIXELFORMAT_R8G8B8X8 = 507,
ADRENO_PIXELFORMAT_R8G8B8 = 508,
ADRENO_PIXELFORMAT_A1B5G5R5 = 519,
ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520,
ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521,
ADRENO_PIXELFORMAT_A2B10G10R10_UNORM = 532,
// Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R10G10B10X2_UNORM = 537,
ADRENO_PIXELFORMAT_D24_UNORM_X8_UINT = 548,
ADRENO_PIXELFORMAT_D24_UNORM = 549,
ADRENO_PIXELFORMAT_D32_FLOAT_X24S8_UINT = 551,
ADRENO_PIXELFORMAT_S8_UINT = 552,
ADRENO_PIXELFORMAT_ASTC_4X4 = 568, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X4 = 569, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X5 = 570, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X5 = 571, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X6 = 572, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X5 = 573, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X6 = 574, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X8 = 575, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X5 = 576, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X6 = 577, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X8 = 578, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X10 = 579, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X10 = 580, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X12 = 581, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, // ASTC Compressed
// Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R5G6B5 = 610, // RGBA version of B5G6R5
ADRENO_PIXELFORMAT_R5G5B5A1 = 611, // RGBA version of B5G5R5A1
ADRENO_PIXELFORMAT_R4G4B4A4 = 612, // RGBA version of B4G4R4A4
ADRENO_PIXELFORMAT_UYVY = 614, // YUV 4:2:2 packed progressive (1 plane)
ADRENO_PIXELFORMAT_ASTC_4X4 = 568,
ADRENO_PIXELFORMAT_ASTC_5X4 = 569,
ADRENO_PIXELFORMAT_ASTC_5X5 = 570,
ADRENO_PIXELFORMAT_ASTC_6X5 = 571,
ADRENO_PIXELFORMAT_ASTC_6X6 = 572,
ADRENO_PIXELFORMAT_ASTC_8X5 = 573,
ADRENO_PIXELFORMAT_ASTC_8X6 = 574,
ADRENO_PIXELFORMAT_ASTC_8X8 = 575,
ADRENO_PIXELFORMAT_ASTC_10X5 = 576,
ADRENO_PIXELFORMAT_ASTC_10X6 = 577,
ADRENO_PIXELFORMAT_ASTC_10X8 = 578,
ADRENO_PIXELFORMAT_ASTC_10X10 = 579,
ADRENO_PIXELFORMAT_ASTC_12X10 = 580,
ADRENO_PIXELFORMAT_ASTC_12X12 = 581,
ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582,
ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583,
ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584,
ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585,
ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586,
ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587,
ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588,
ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589,
ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590,
ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591,
ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592,
ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593,
ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594,
ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595,
ADRENO_PIXELFORMAT_R5G6B5 = 610,
ADRENO_PIXELFORMAT_R5G5B5A1 = 611,
ADRENO_PIXELFORMAT_R4G4B4A4 = 612,
ADRENO_PIXELFORMAT_UYVY = 614,
ADRENO_PIXELFORMAT_NV21 = 619,
ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, // YUV 4:4:4 packed (1 plane)
ADRENO_PIXELFORMAT_Y8 = 625, // Single 8-bit luma only channel YUV format
ADRENO_PIXELFORMAT_TP10 = 654, // YUV 4:2:0 planar 10 bits/comp (2 planes)
ADRENO_PIXELFORMAT_Y8U8V8A8 = 620,
ADRENO_PIXELFORMAT_Y8 = 625,
ADRENO_PIXELFORMAT_TP10 = 654,
} ADRENOPIXELFORMAT;
typedef enum {
SURFACE_TILE_MODE_DISABLE = 0x0, // used for linear surface
SURFACE_TILE_MODE_ENABLE = 0x1 // used for tiled surface
SURFACE_TILE_MODE_DISABLE = 0x0,
SURFACE_TILE_MODE_ENABLE = 0x1
} surface_tile_mode_t;
class AdrenoMemInfo {
public:
/*
* Function to compute aligned width and aligned height based on
* width, height, format and usage flags.
*
* @return aligned width, aligned height
*/
void GetAlignedWidthAndHeight(int width, int height, int format, int usage,
unsigned int* aligned_w, unsigned int* aligned_h, bool ubwc_enabled,
bool tile_enabled);
/*
* Function to compute the adreno aligned width and aligned height
* based on the width and format.
*
* @return aligned width, aligned height
*/
void AlignUnCompressedRGB(int width, int height, int format, int tileEnabled,
unsigned int* aligned_w, unsigned int* aligned_h);
/*
* Function to compute the adreno aligned width and aligned height
* based on the width and format.
*
* @return aligned width, aligned height
*/
void AlignCompressedRGB(int width, int height, int format, unsigned int* aligned_w,
unsigned int* aligned_h);
/*
* Function to compute the pixel alignment requirement.
*
* @return alignment
*/
uint32_t GetGpuPixelAlignment();
/*
* Function to query whether GPU supports UBWC for given HAL format
* @return > 0 : supported
* 0 : not supported
*/
bool IsUBWCSupportedByGPU(int format);
/*
* Function to check if GPU supports PI or not
*/
bool IsPISupportedByGPU(int format, uint64_t usage);
/*
* Function to get the corresponding Adreno format for given HAL format
*/
ADRENOPIXELFORMAT GetGpuPixelFormat(int hal_format);
/*
* Function to get graphics metadata blob size
* @return graphics metadata size
*/
uint32_t AdrenoGetMetadataBlobSize();
/*
* Function to populate the graphics metadata blob
* @return 1 : Successful
* 2 : Unsuccessful
*/
int AdrenoInitMemoryLayout(void* metadata_blob, int width, int height, int depth, int format,
int num_samples, int isUBWC, uint64_t usage, uint32_t num_planes);
/*
* Function to get buffer size for based on graphcis metadata
* @return buffer size
*/
uint32_t AdrenoGetAlignedGpuBufferSize(void* metadata_blob);
/*
* Function to check if adreno size calculation APIs are avaliable
* @return true : Avaliable
* false : Unavaliable
*/
bool AdrenoSizeAPIAvaliable();
static AdrenoMemInfo* GetInstance();
@@ -196,7 +142,6 @@ class AdrenoMemInfo {
private:
AdrenoMemInfo();
~AdrenoMemInfo();
// link(s)to adreno surface padding library.
int (*LINK_adreno_compute_padding)(int width, int bpp, int surface_tile_height,
int screen_tile_height, int padding_threshold) = NULL;
void (*LINK_adreno_compute_aligned_width_and_height)(int width, int height, int bpp,
@@ -229,6 +174,6 @@ class AdrenoMemInfo {
static AdrenoMemInfo* s_instance;
};
} // namespace gralloc
}
#endif // __GR_ADRENO_INFO_H__
#endif
@@ -57,29 +57,27 @@ struct private_handle_t {
PRIV_FLAGS_CAMERA_READ = 0x00040000,
PRIV_FLAGS_HW_COMPOSER = 0x00080000,
PRIV_FLAGS_HW_TEXTURE = 0x00100000,
PRIV_FLAGS_ITU_R_601 = 0x00200000, // Unused from display
PRIV_FLAGS_ITU_R_601_FR = 0x00400000, // Unused from display
PRIV_FLAGS_ITU_R_709 = 0x00800000, // Unused from display
PRIV_FLAGS_ITU_R_601 = 0x00200000,
PRIV_FLAGS_ITU_R_601_FR = 0x00400000,
PRIV_FLAGS_ITU_R_709 = 0x00800000,
PRIV_FLAGS_SECURE_DISPLAY = 0x01000000,
PRIV_FLAGS_TILE_RENDERED = 0x02000000,
PRIV_FLAGS_CPU_RENDERED = 0x04000000,
PRIV_FLAGS_UBWC_ALIGNED = 0x08000000,
PRIV_FLAGS_DISP_CONSUMER = 0x10000000,
PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, // Ion buffer allocated outside of gralloc
PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, // PI format
PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000,
PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000,
};
// file-descriptors dup'd over IPC
int fd;
int fd_metadata;
// values sent over IPC
int magic;
int flags;
int width; // holds width of the actual buffer allocated
int height; // holds height of the actual buffer allocated
int unaligned_width; // holds width client asked to allocate
int unaligned_height; // holds height client asked to allocate
int width;
int height;
int unaligned_width;
int unaligned_height;
int format;
int buffer_type;
unsigned int layer_count;
@@ -126,7 +124,6 @@ struct private_handle_t {
numFds = kNumFds;
}
// Legacy constructor used by some clients
private_handle_t(int fd, unsigned int size, int usage, int buf_type, int format, int w, int h)
: private_handle_t(fd, -1, PRIV_FLAGS_CLIENT_ALLOCATED, w, h, 0, 0, format, buf_type, size,
static_cast<uint64_t>(usage)) {}
@@ -176,7 +173,6 @@ struct private_handle_t {
unsigned int GetLayerCount() const { return layer_count; }
int GetStride() const {
// In handle we currently store aligned width after allocation.
return width;
}
@@ -187,4 +183,4 @@ struct private_handle_t {
};
#pragma pack(pop)
#endif // __GR_PRIV_HANDLE_H__
#endif
@@ -162,7 +162,6 @@ bool CpuCanRead(uint64_t usage) {
bool CpuCanWrite(uint64_t usage) {
if (usage & BufferUsage::CPU_WRITE_MASK) {
// Application intends to use CPU for rendering
return true;
}
@@ -177,7 +176,6 @@ uint32_t GetDataAlignment(int format, uint64_t usage) {
if (usage & BufferUsage::PROTECTED) {
if ((usage & BufferUsage::CAMERA_OUTPUT) || (usage & GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY)) {
// The alignment here reflects qsee mmu V7L/V8L requirement
align = SZ_2M;
} else {
align = SECURE_ALIGN;
@@ -202,7 +200,6 @@ bool IsGPUFlagSupported(uint64_t usage) {
return ret;
}
// Returns the final buffer size meant to be allocated with ion
unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int alignedh) {
unsigned int size = 0;
int format = info.format;
@@ -223,7 +220,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
} else if (IsCompressedRGBFormat(format)) {
size = alignedw * alignedh * ASTC_BLOCK_SIZE;
} else {
// Below switch should be for only YUV/custom formats
switch (format) {
case HAL_PIXEL_FORMAT_RAW16:
case HAL_PIXEL_FORMAT_Y16:
@@ -237,15 +233,15 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
case HAL_PIXEL_FORMAT_Y8:
size = alignedw * alignedh * 1;
break;
// adreno formats
case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: // NV21
case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO:
size = ALIGN(alignedw * alignedh, SIZE_4K);
size += (unsigned int)ALIGN(2 * ALIGN(width / 2, 32) * ALIGN(height / 2, 32), SIZE_4K);
break;
case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: // NV12
// The chroma plane is subsampled,
// but the pitch in bytes is unchanged
// The GPU needs 4K alignment, but the video decoder needs 8K
case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED:
size = ALIGN(alignedw * alignedh, SIZE_8K);
size += ALIGN(alignedw * (unsigned int)ALIGN(height / 2, 32), SIZE_8K);
break;
@@ -296,7 +292,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
return size;
}
// Explicitly defined UBWC formats
bool IsUBwcFormat(int format) {
switch (format) {
case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC:
@@ -309,7 +304,6 @@ bool IsUBwcFormat(int format) {
}
bool IsUBwcSupported(int format) {
// Existing HAL formats with UBWC support
switch (format) {
case HAL_PIXEL_FORMAT_BGR_565:
case HAL_PIXEL_FORMAT_RGBA_8888:
@@ -336,7 +330,6 @@ bool IsUBwcPISupported(int format, uint64_t usage) {
return false;
}
// As of now only two formats
switch (format) {
case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC:
case HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC: {
@@ -354,27 +347,21 @@ bool IsUBwcPISupported(int format, uint64_t usage) {
}
bool IsUBwcEnabled(int format, uint64_t usage) {
// Allow UBWC, if client is using an explicitly defined UBWC pixel format.
if (IsUBwcFormat(format)) {
return true;
}
// Allow UBWC, if an OpenGL client sets UBWC usage flag and GPU plus MDP
// support the format. OR if a non-OpenGL client like Rotator, sets UBWC
// usage flag and MDP supports the format.
if (((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC) ||
(usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) ||
(usage & BufferUsage::COMPOSER_CLIENT_TARGET)) &&
IsUBwcSupported(format)) {
bool enable = true;
// Query GPU for UBWC only if buffer is intended to be used by GPU.
if ((usage & BufferUsage::GPU_TEXTURE) || (usage & BufferUsage::GPU_RENDER_TARGET)) {
if (AdrenoMemInfo::GetInstance()) {
enable = AdrenoMemInfo::GetInstance()->IsUBWCSupportedByGPU(format);
}
}
// Allow UBWC, only if CPU usage flags are not set
if (enable && !(CpuCanAccess(usage))) {
return true;
}
@@ -418,13 +405,10 @@ unsigned int GetRgbUBwcMetaBufferSize(int width, int height, uint32_t bpp) {
return size;
}
// Align meta buffer height to 16 blocks
meta_height = ALIGN(((height + block_height - 1) / block_height), 16);
// Align meta buffer width to 64 blocks
meta_width = ALIGN(((width + block_width - 1) / block_width), 64);
// Align meta buffer size to 4K
size = (unsigned int)ALIGN((meta_width * meta_height), 4096);
return size;
@@ -500,12 +484,10 @@ bool UseUncached(int format, uint64_t usage) {
return true;
}
// CPU read rarely
if ((usage & BufferUsage::CPU_READ_MASK) == static_cast<uint64_t>(BufferUsage::CPU_READ_RARELY)) {
return true;
}
// CPU write rarely
if ((usage & BufferUsage::CPU_WRITE_MASK) ==
static_cast<uint64_t>(BufferUsage::CPU_WRITE_RARELY)) {
return true;
@@ -576,8 +558,6 @@ uint64_t GetHandleFlags(int format, uint64_t usage) {
int GetImplDefinedFormat(uint64_t usage, int format) {
int gr_format = format;
// If input format is HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED then based on
// the usage bits, gralloc assigns a format.
if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED ||
format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
if ((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC || usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) &&
@@ -585,33 +565,30 @@ int GetImplDefinedFormat(uint64_t usage, int format) {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC;
} else if (usage & BufferUsage::VIDEO_ENCODER) {
if (usage & GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER) {
gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE;
} else if (usage & GRALLOC_USAGE_PRIVATE_HEIF) {
gr_format = HAL_PIXEL_FORMAT_NV12_HEIF;
} else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS;
} else {
gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; // NV12
gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE;
}
} else if (usage & BufferUsage::CAMERA_INPUT) {
if (usage & BufferUsage::CAMERA_OUTPUT) {
// Assumed ZSL if both producer and consumer camera flags set
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
} else {
gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; // NV21
gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP;
}
} else if (usage & BufferUsage::CAMERA_OUTPUT) {
if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
} else {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; // NV12 preview
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS;
}
} else if (usage & BufferUsage::COMPOSER_OVERLAY) {
// XXX: If we still haven't set a format, default to RGBA8888
gr_format = HAL_PIXEL_FORMAT_RGBA_8888;
} else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
// If no other usage flags are detected, default the
// flexible YUV format to NV21_ZSL
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
}
}
@@ -637,7 +614,7 @@ bool HasAlphaComponent(int32_t format) {
}
void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int32_t height,
int32_t /* flags */, int* plane_count, PlaneLayoutInfo* plane_info) {
int32_t , int* plane_count, PlaneLayoutInfo* plane_info) {
uint64_t usage = info.usage;
*plane_count = 1;
uint32_t bpp = 0;
@@ -659,4 +636,4 @@ void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int3
plane_info->scanlines = height;
}
} // namespace gralloc
}
@@ -42,7 +42,7 @@
#ifdef SLAVE_SIDE_CP
#define SECURE_ALIGN SZ_1M
#else // MASTER_SIDE_CP
#else
#define SECURE_ALIGN SZ_4K
#endif
@@ -68,59 +68,38 @@ inline Type1 ALIGN(Type1 x, Type2 align) {
}
enum PlaneComponent {
/* luma */
PLANE_COMPONENT_Y = 1 << 0,
/* chroma blue */
PLANE_COMPONENT_Cb = 1 << 1,
/* chroma red */
PLANE_COMPONENT_Cr = 1 << 2,
/* red */
PLANE_COMPONENT_R = 1 << 10,
/* green */
PLANE_COMPONENT_G = 1 << 11,
/* blue */
PLANE_COMPONENT_B = 1 << 12,
/* alpha */
PLANE_COMPONENT_A = 1 << 20,
/* raw data plane */
PLANE_COMPONENT_RAW = 1 << 30,
/* meta information plane */
PLANE_COMPONENT_META = 1 << 31,
};
struct PlaneLayoutInfo {
/** Components represented the type of plane. */
PlaneComponent component;
/** horizontal subsampling. Must be a positive power of 2. */
uint32_t h_subsampling;
/** vertical subsampling. Must be a positive power of 2. */
uint32_t v_subsampling;
/** offset to the first byte of the top-left pixel of the plane
* and it is calculated from the start of the buffer.
* Add base of the handle with offset to get the first byte of the plane.
*/
uint32_t offset;
/** step is the distance in bytes from one pixel value to the next. */
int32_t step;
/** stride of the plane in pixels */
int32_t stride;
/** stride of the plane in in bytes */
int32_t stride_bytes;
/** plane height or vertical stride */
int32_t scanlines;
/** size of the plane in bytes */
uint32_t size;
};
@@ -151,6 +130,6 @@ int GetImplDefinedFormat(uint64_t usage, int format);
int GetCustomFormatFlags(int format, uint64_t usage, int* custom_format, uint64_t* priv_flags);
bool IsGPUFlagSupported(uint64_t usage);
bool HasAlphaComponent(int32_t format);
} // namespace gralloc
}
#endif // __GR_UTILS_H__
#endif
@@ -26,7 +26,6 @@
#include "QtiGrallocDefs.h"
#include "gr_priv_handle.h"
//#pragma message "Warning: gralloc_priv.h is deprecated"
#define GRALLOC_PROP_PREFIX "vendor.gralloc."
#define GRALLOC_PROP(prop_name) GRALLOC_PROP_PREFIX prop_name
@@ -42,8 +41,6 @@ inline int roundUpToPageSize(int x) {
return (x + (getpagesize() - 1)) & ~(getpagesize() - 1);
}
/* Legacy gralloc1 definitions */
/* Some clients may still be using the old flags */
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_ADSP_HEAP GRALLOC_USAGE_PRIVATE_ADSP_HEAP
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_ALLOC_UBWC GRALLOC_USAGE_PRIVATE_ALLOC_UBWC
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_UNCACHED GRALLOC_USAGE_PRIVATE_UNCACHED
@@ -54,7 +51,6 @@ inline int roundUpToPageSize(int x) {
#define GRALLOC1_CONSUMER_USAGE_PRIVATE_10BIT_TP GRALLOC_USAGE_PRIVATE_10BIT_TP
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_VIDEO_NV21_ENCODER GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER
// for PERFORM API :
#define GRALLOC_MODULE_PERFORM_CREATE_HANDLE_FROM_BUFFER 1
#define GRALLOC_MODULE_PERFORM_GET_STRIDE 2
#define GRALLOC_MODULE_PERFORM_GET_CUSTOM_STRIDE_FROM_HANDLE 3
@@ -72,11 +68,9 @@ inline int roundUpToPageSize(int x) {
#define GRALLOC1_MODULE_PERFORM_GET_INTERLACE_FLAG 15
#define GRALLOC_MODULE_PERFORM_GET_GRAPHICS_METADATA 16
/* possible values for inverse gamma correction */
#define HAL_IGC_NOT_SPECIFIED 0
#define HAL_IGC_s_RGB 1
/* Color Space: Values maps to ColorSpace_t in qdMetadata.h */
#define HAL_CSC_ITU_R_601 0
#define HAL_CSC_ITU_R_601_FR 1
#define HAL_CSC_ITU_R_709 2
@@ -84,20 +78,16 @@ inline int roundUpToPageSize(int x) {
#define HAL_CSC_ITU_R_2020 4
#define HAL_CSC_ITU_R_2020_FR 5
/* possible formats for 3D content*/
enum {
HAL_NO_3D = 0x0,
HAL_3D_SIDE_BY_SIDE_L_R = 0x1,
HAL_3D_SIDE_BY_SIDE_R_L = 0x2,
HAL_3D_TOP_BOTTOM = 0x4,
HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, // unused legacy format
HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000,
};
/* Flag to determine interlaced content
* Value maps to Flags presents in types.hal of QtiMapperextensions
*/
enum {
LAYOUT_INTERLACED_FLAG = 1 << 0,
};
#endif // __GRALLOC_PRIV_H__
#endif
@@ -60,43 +60,43 @@ struct IMapper : public IBase {
int32_t height __attribute__((aligned(4)));
};
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V2_0
}
namespace V3_0 {
struct IMapper : public IBase {
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V3_0
}
namespace V4_0 {
struct IMapper : public IBase {
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V4_0
} // namespace android::hardware::graphics::mapper
}
}
namespace vendor::qti::hardware::display::mapper {
namespace V4_0 {
struct IQtiMapper : public IBase {
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V4_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V4_0::IMapper>& ,
bool );
};
} // namespace V4_0
}
namespace V3_0 {
struct IQtiMapper : public IBase {
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V3_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V3_0::IMapper>& ,
bool );
};
} // namespace V3_0
}
namespace V2_0 {
struct IQtiMapper : public IBase {
@@ -175,9 +175,9 @@ struct IQtiMapper : public IBase {
V1_0::IQtiMapperExtensions>& extensions)>;
virtual Return<void> getMapperExtensions(getMapperExtensions_cb _hidl_cb);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V2_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V2_0::IMapper>& ,
bool );
};
} // namespace V2_0
} // namespace vendor::qti::hardware::display::mapper
}
}
@@ -45,7 +45,7 @@ struct IQtiMapperExtensions : public IBase {
IQtiMapperExtensions(){};
};
} // namespace V1_0
}
namespace V1_1 {
using Error = V1_0::Error;
using PlaneLayout = V1_0::PlaneLayout;
@@ -64,7 +64,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough
using getInterlacedFlag_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, bool interlaced)>;
virtual ::android::hardware::Return<void> getInterlacedFlag(void* buffer,
@@ -79,7 +78,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough
using getRgbDataAddress_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, void* rgb_data)>;
virtual ::android::hardware::Return<void> getRgbDataAddress(void* buffer,
@@ -95,7 +93,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passhthrough
using getColorSpace_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t color_space)>;
virtual ::android::hardware::Return<void> getColorSpace(void* buffer, getColorSpace_cb _hidl_cb) {
@@ -103,7 +100,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough with hidl fixups
using getYuvPlaneInfo_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error,
const ::android::hardware::hidl_vec<
@@ -130,27 +126,22 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, cast private data with no passthrough
using getFd_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t fd)>;
virtual ::android::hardware::Return<void> getFd(void* buffer, getFd_cb _hidl_cb);
// Used, cast private data with no passthrough
using getWidth_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t width)>;
virtual ::android::hardware::Return<void> getWidth(void* buffer, getWidth_cb _hidl_cb);
// Used, cast private data with no passthrough
using getHeight_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t height)>;
virtual ::android::hardware::Return<void> getHeight(void* buffer, getHeight_cb _hidl_cb);
// Used, cast private data with no passthrough
using getOffset_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t offset)>;
virtual ::android::hardware::Return<void> getOffset(void* buffer, getOffset_cb _hidl_cb);
// Used, cast private data with no passthrough
using getSize_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t size)>;
virtual ::android::hardware::Return<void> getSize(void* buffer, getSize_cb _hidl_cb);
@@ -179,12 +170,10 @@ struct IQtiMapperExtensions : public IBase {
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t usage)>;
virtual ::android::hardware::Return<void> getUsageFlags(void* buffer, getUsageFlags_cb _hidl_cb);
// Used, cast private data with no passthrough
using getFormat_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t format)>;
virtual ::android::hardware::Return<void> getFormat(void* buffer, getFormat_cb _hidl_cb);
// Used, cast private data with no passthrough
using getPrivateFlags_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t flags)>;
virtual ::android::hardware::Return<void> getPrivateFlags(void* buffer,
@@ -198,7 +187,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, PAIN
using getFormatLayout_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t size,
const ::android::hardware::hidl_vec<
@@ -208,7 +196,6 @@ struct IQtiMapperExtensions : public IBase {
int32_t height,
getFormatLayout_cb _hidl_cb);
// V1.1, Used, passthrough
virtual ::android::hardware::Return<
::vendor::qti::hardware::display::mapperextensions::V1_0::Error>
getSurfaceMetadata_V1(void* buffer, void* metadata) {
@@ -217,8 +204,8 @@ struct IQtiMapperExtensions : public IBase {
}
static Return<sp<IQtiMapperExtensions>> castFrom(
android::sp<V1_0::IQtiMapperExtensions> const& /* parent */, bool /* emitError */);
android::sp<V1_0::IQtiMapperExtensions> const& , bool );
};
} // namespace V1_1
} // namespace vendor::qti::hardware::display::mapperextensions
}
}
File diff suppressed because it is too large. Load diff
@@ -12,7 +12,6 @@ namespace graphics {
namespace mapper {
namespace V2_0 {
// Forward declaration for forward reference support:
enum class Error : int32_t;
struct YCbCrLayout;
@@ -25,34 +24,8 @@ enum class Error : int32_t {
UNSUPPORTED = 7,
};
/**
* A buffer descriptor is an implementation-defined opaque data returned by
* createDescriptor. It describes the properties of a buffer and is consumed
* by the allocator.
*/
typedef ::android::hardware::hidl_vec<uint32_t> BufferDescriptor;
/**
* Structure for describing YCbCr formats for consumption by applications.
* This is used with PixelFormat::YCBCR_*_888.
*
* Buffer chroma subsampling is defined in the format.
* e.g. PixelFormat::YCBCR_420_888 has subsampling 4:2:0.
*
* Buffers must have a 8 bit depth.
*
* y, cb, and cr point to the first byte of their respective planes.
*
* Stride describes the distance in bytes from the first value of one row of
* the image to the first value of the next row. It includes the width of the
* image plus padding.
* yStride is the stride of the luma plane.
* cStride is the stride of the chroma planes.
*
* chromaStep is the distance in bytes from one chroma pixel value to the
* next. This is 2 bytes for semiplanar (because chroma values are interleaved
* and each chroma value is one byte) and 1 for planar.
*/
struct YCbCrLayout final {
void* y;
void* cb;
@@ -62,9 +35,6 @@ struct YCbCrLayout final {
uint32_t chromaStep;
};
//
// type declarations for package
//
template <typename>
static inline std::string toString(int32_t o);
@@ -107,11 +77,7 @@ constexpr int32_t& operator&=(int32_t& v,
static inline std::string toString(
const ::android::hardware::graphics::mapper::V2_0::YCbCrLayout& o);
// operator== and operator!= are not generated for YCbCrLayout
//
// type header definitions for package
//
template <>
inline std::string toString<::android::hardware::graphics::mapper::V2_0::Error>(int32_t o) {
@@ -218,17 +184,17 @@ static inline std::string toString(
return os;
}
// operator== and operator!= are not generated for YCbCrLayout
} // namespace V2_0
} // namespace mapper
} // namespace graphics
} // namespace hardware
} // namespace android
//
// global type declarations for package
//
}
}
}
}
}
namespace android {
namespace hardware {
@@ -243,8 +209,8 @@ constexpr std::array<::android::hardware::graphics::mapper::V2_0::Error, 6>
::android::hardware::graphics::mapper::V2_0::Error::NO_RESOURCES,
::android::hardware::graphics::mapper::V2_0::Error::UNSUPPORTED,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_MAPPER_V2_0_TYPES_H
#endif
@@ -13,7 +13,6 @@ namespace display {
namespace mapperextensions {
namespace V1_0 {
// Forward declaration for forward reference support:
enum class Error : int32_t;
struct YCbCrLayout;
enum class PlaneComponent : uint32_t;
@@ -36,61 +35,22 @@ struct YCbCrLayout final {
uint32_t chromaStep;
};
/**
* Plane Component type definitions.
*/
enum class PlaneComponent : uint32_t {
PLANE_COMPONENT_Y = 1u /* (1 << 0) */,
PLANE_COMPONENT_Cb = 2u /* (1 << 1) */,
PLANE_COMPONENT_Cr = 4u /* (1 << 2) */,
PLANE_COMPONENT_R = 1024u /* (1 << 10) */,
PLANE_COMPONENT_G = 2048u /* (1 << 11) */,
PLANE_COMPONENT_B = 4096u /* (1 << 12) */,
PLANE_COMPONENT_A = 1048576u /* (1 << 20) */,
PLANE_COMPONENT_RAW = 1073741824u /* (1 << 30) */,
PLANE_COMPONENT_META = 2147483648u /* (1 << 31) */,
PLANE_COMPONENT_Y = 1u ,
PLANE_COMPONENT_Cb = 2u ,
PLANE_COMPONENT_Cr = 4u ,
PLANE_COMPONENT_R = 1024u ,
PLANE_COMPONENT_G = 2048u ,
PLANE_COMPONENT_B = 4096u ,
PLANE_COMPONENT_A = 1048576u ,
PLANE_COMPONENT_RAW = 1073741824u ,
PLANE_COMPONENT_META = 2147483648u ,
};
/**
* Flags to send with getFormatLayout for extra information for e.g. interlaced,
* otherwise send 0.
*/
enum class Flags : int32_t {
LAYOUT_INTERLACED_FLAG = 1 /* (1 << 0) */,
LAYOUT_INTERLACED_FLAG = 1 ,
};
/**
* Structure for describing plane layout information for YUVA/RGBA/RAW formats
* YUV formats can be planar or semi-planar.
*
* This layout structure is used with any pixel format that can be
* represented by it, such as:
* - HAL_PIXEL_FORMAT_YCbCr_*
* - HAL_PIXEL_FORMAT_RGB*_*
* - even implementation defined formats that can be represented by
* the structures
*
* Each plane can be made up of one or more PlaneComponents depending on
* the format.
*
* Each plane can be subsampled either vertically or horizontally by
* a power-of-two factor.
*
* Pixel increment describes the distance in bytes from one pixel to the
* next pixel (to the right) on the same row for the component plane.
* This can be negative.
*
* Stride is provided in both pixels and bytes.
* Stride_bytes describes the distance in bytes from the first pixel of
* one row to the first pixel of the next row (below) for the component plane.
* This can be negative.
*
* Scanlines describes the height of the plane in terms of number of rows.
*
* Size of plane is stride_bytes * scanlines + any padding required for
* alignment as per format specification.
*
*/
struct PlaneLayout final {
::android::hardware::hidl_bitfield<
::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent>
@@ -137,9 +97,6 @@ static_assert(sizeof(::vendor::qti::hardware::display::mapperextensions::V1_0::P
static_assert(__alignof(::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout) == 4,
"wrong alignment");
//
// type declarations for package
//
template <typename>
static inline std::string toString(int32_t o);
@@ -185,7 +142,6 @@ constexpr int32_t& operator&=(
static inline std::string toString(
const ::vendor::qti::hardware::display::mapperextensions::V1_0::YCbCrLayout& o);
// operator== and operator!= are not generated for YCbCrLayout
template <typename>
static inline std::string toString(uint32_t o);
@@ -284,9 +240,6 @@ static inline bool operator!=(
const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& lhs,
const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& rhs);
//
// type header definitions for package
//
template <>
inline std::string toString<::vendor::qti::hardware::display::mapperextensions::V1_0::Error>(
@@ -379,7 +332,6 @@ static inline std::string toString(
return os;
}
// operator== and operator!= are not generated for YCbCrLayout
template <>
inline std::string
@@ -642,16 +594,13 @@ static inline bool operator!=(
return !(lhs == rhs);
}
} // namespace V1_0
} // namespace mapperextensions
} // namespace display
} // namespace hardware
} // namespace qti
} // namespace vendor
}
}
}
}
}
}
//
// global type declarations for package
//
namespace android {
namespace hardware {
@@ -664,9 +613,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::E
::vendor::qti::hardware::display::mapperextensions::V1_0::Error::NO_RESOURCES,
::vendor::qti::hardware::display::mapperextensions::V1_0::Error::UNSUPPORTED,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
namespace android {
namespace hardware {
@@ -688,9 +637,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::P
::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent::
PLANE_COMPONENT_META,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
namespace android {
namespace hardware {
@@ -700,8 +649,8 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::F
hidl_enum_values<::vendor::qti::hardware::display::mapperextensions::V1_0::Flags> = {
::vendor::qti::hardware::display::mapperextensions::V1_0::Flags::LAYOUT_INTERLACED_FLAG,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
#endif // HIDL_GENERATED_VENDOR_QTI_HARDWARE_DISPLAY_MAPPEREXTENSIONS_V1_0_TYPES_H
#endif
@@ -46,14 +46,13 @@
using namespace android::hardware;
using namespace android;
// These two functions are used by the blob and aren't present on Q so weakly stub them
extern "C" uint64_t __attribute__((weak)) atrace_get_enabled_tags() {
return 0;
}
namespace android::hardware::details {
void __attribute__((weak)) return_status::onValueRetrieval() const {}
} // namespace android::hardware::details
}
bool IBase::isRemote() const {
return false;
@@ -146,7 +145,7 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V2_0
}
namespace V3_0 {
sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getStub) {
@@ -154,7 +153,7 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V3_0
}
namespace V4_0 {
sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getStub) {
@@ -162,8 +161,8 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V4_0
} // namespace android::hardware::graphics::mapper
}
}
namespace vendor::qti::hardware::display {
namespace mapper {
namespace V4_0 {
@@ -174,7 +173,7 @@ Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V4_0::IMa
return nullptr;
}
} // namespace V4_0
}
namespace V3_0 {
Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V3_0::IMapper>& parent,
@@ -183,7 +182,7 @@ Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V3_0::IMa
return nullptr;
}
} // namespace V3_0
}
namespace V2_0 {
Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V2_0::IMapper>& parent,
@@ -200,8 +199,8 @@ Return<void> IQtiMapper::getMapperExtensions(getMapperExtensions_cb _hidl_cb) {
return Void();
}
} // namespace V2_0
} // namespace mapper
}
}
namespace mapperextensions {
namespace V1_1 {
@@ -467,6 +466,6 @@ Return<void> IQtiMapperExtensions::getFormatLayout(int32_t format, uint64_t usag
return Void();
}
} // namespace V1_1
} // namespace mapperextensions
} // namespace vendor::qti::hardware::display
}
}
}
-5
View File
@@ -1,6 +1,3 @@
// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one
// instance on the system loader, the first physical device's properties, a few limits and the
// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open.
#include <dlfcn.h>
#include <jni.h>
#include <stdio.h>
@@ -62,12 +59,10 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
LINE("device=%s\n", p.deviceName);
LINE("type=%s\n", type_name(p.deviceType));
LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion));
// Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION.
LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff);
LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID);
LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D);
// Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties).
uint32_t extCount = 0;
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
static VkExtensionProperties exts[512];
File diff suppressed because it is too large. Load diff
+85 -243
View File
@@ -84,12 +84,11 @@ struct VkTable {
PFN_vkCmdSetScissor CmdSetScissor;
PFN_vkCmdPipelineBarrier CmdPipelineBarrier;
PFN_vkCmdCopyImage CmdCopyImage;
PFN_vkCmdBlitImage CmdBlitImage; // frame-gen capture resolution: composite -> swapchain upscale
// Compute: the native LSFG chain is 25 compute dispatches.
PFN_vkCmdBlitImage CmdBlitImage;
PFN_vkCmdDispatch CmdDispatch;
PFN_vkCreateComputePipelines CreateComputePipelines;
PFN_vkUnmapMemory UnmapMemory;
// win-fg native: clears its flow scratch, resets its scratch pool on resize.
PFN_vkCmdClearColorImage CmdClearColorImage;
PFN_vkResetDescriptorPool ResetDescriptorPool;
PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage;
@@ -125,11 +124,8 @@ struct VkTable {
#include <condition_variable>
#include <chrono>
// Renderer-neutral direct-scanout impl (owns the SurfaceControl/AHB state).
// Included after SCANOUT_LOG is defined above; its own definition is guarded.
#include "../scanout/ScanoutContext.h"
// Native (compositor-side) LSFG frame generation - capability gate.
#include "lsfg/lsfg_probe.h"
#include <memory>
#include <string>
@@ -139,89 +135,83 @@ namespace winfg { class Engine; }
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
// Push-constant layouts for the spatial-upscaler post passes. The leading vec4
// `ndc` matches upscale.vert; the remaining members are read only by the
// fragment stage. std430 offsets line up with these C structs.
struct SgsrPushConstants { // 36 bytes
struct SgsrPushConstants {
float ndc[4];
float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px
float edgeSharpness; // SGSR EdgeSharpness, from the slider
float viewportInfo[4];
float edgeSharpness;
};
struct NisPushConstants { // 36 bytes
struct NisPushConstants {
float ndc[4];
float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px
float sharpness; // NIS sharpness [0..1], from the slider
float viewportInfo[4];
float sharpness;
};
struct EasuPushConstants { // 88 bytes
struct EasuPushConstants {
float ndc[4];
uint32_t con0[4], con1[4], con2[4], con3[4];
float outW, outH;
};
struct RcasPushConstants { // 40 bytes
struct RcasPushConstants {
float ndc[4];
uint32_t con[4]; // con.x = bit-packed sharpness
uint32_t con[4];
float outW, outH;
};
// Composable post effects (AMD CAS sharpen + fake-HDR). Both lead with vec4 ndc
// (offset 0) like the upscaler PCs and stay well under the 88-byte PC range.
struct CasPushConstants { // 32 bytes
struct CasPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float sharpness; // CAS SHARPNESS term [0..1]
float resolution[2];
float sharpness;
float _pad;
};
struct HdrPushConstants { // 24 bytes
struct HdrPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
// Phase 2 screen effects ported from the GL EffectComposer. Each leads with vec4
// ndc (offset 0) like the upscaler PCs and stays well under the 88-byte PC range.
struct FxaaPushConstants { // 24 bytes
struct FxaaPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
struct ToonPushConstants { // 24 bytes
struct ToonPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
struct ColorPushConstants { // 32 bytes
struct ColorPushConstants {
float ndc[4];
float brightness; // additive [-1,1]
float contrast; // [0,2]
float gamma; // [0.1,5]
float saturation; // [0,2]; 1 = unchanged, 0 = greyscale.
// APPENDED last so the three offsets above
// stay where color.frag already had them.
float brightness;
float contrast;
float gamma;
float saturation;
};
struct NtscPushConstants { // 28 bytes
struct NtscPushConstants {
float ndc[4];
float resolution[2]; // screen size in px (TextureSize/resolution)
float frameCount; // animated chroma-phase counter
float resolution[2];
float frameCount;
};
struct CrtPushConstants { // 24 bytes
struct CrtPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px (unused by shader)
float resolution[2];
};
struct DebandPushConstants { // 28 bytes
struct DebandPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px (layout parity)
float strength; // dither amplitude in LSBs (1.0 = +/-1/255)
float resolution[2];
float strength;
};
class VulkanRendererContext {
public:
// `lsfgVk11Compat` (experimental) lets the LSFG capability probe accept a
// Vulkan 1.1/1.2 device that offers the extensions the chain needs; it has
// to be known before vkCreateDevice, hence a constructor argument.
VulkanRendererContext(ANativeWindow* window, int cWidth, int cHeight, void* adrenotoolsHandle = nullptr,
bool lsfgVk11Compat = false);
~VulkanRendererContext();
void onSurfaceResized(int width, int height);
void setTransform(float ox, float oy, float sx, float sy);
// #413: game-region clip rect (surface px). Stored and applied as the swapchain scissor in
// recordCmdBuf so a FILL/STRETCH game confined to a half (TOP/BOTTOM alignment) can't bleed into
// the on-screen-controls half. A full-surface (or w<=0) rect means "no clip" (CENTER).
void setClipRegion(int x, int y, int w, int h);
void updatePointerPosition(short x, short y);
void updateWindowContent(int64_t id, void* pixels, short w, short h, short stride, int x, int y);
@@ -241,13 +231,7 @@ public:
void scanoutSetBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1);
void scanoutSetCursorImage(void* pixels, short w, short h, short stride);
void scanoutSetCursorPos(short x, short y, short hotX, short hotY);
// The renderer-neutral scanout implementation. The scanout methods above are
// thin forwarders to this (see VulkanRendererScanout.cpp); the cursor render-
// thread signalling stays in the forwarders, not in ScanoutContext.
ScanoutContext scanout;
// Bound to scanout's atomics so the existing JNI ABI (r->scanoutActive /
// r->gameFrameDelivered direct member access) keeps compiling against a
// single source of truth. Must be declared after `scanout`.
std::atomic<bool>& scanoutActive = scanout.scanoutActive;
std::atomic<bool>& gameFrameDelivered = scanout.gameFrameDelivered;
std::atomic<bool> surfaceDetached{false};
@@ -286,47 +270,16 @@ public:
void setPresentMode(VkPresentModeKHR mode);
std::vector<int> getSupportedPresentModes() const;
// --- Native LSFG frame generation -------------------------------------
// Capability verdict for the compositor-side LSFG engine. Filled at device
// creation and completed once the swapchain format is known; read by the
// UI (through JNI) to grey the engine out with a reason.
const lsfg::Caps& lsfgCaps() const { return lsfgCaps_; }
// Arm/disarm native LSFG frame generation for this session. Changing the
// armed state recreates the swapchain, because the composite path needs
// TRANSFER_DST usage and a deeper image queue that a normal session does
// not pay for.
void setFrameGenArmed(bool armed, int multiplier);
bool frameGenArmed() const { return fgArmed_.load(std::memory_order_relaxed); }
// Live frame-gen telemetry for the in-game readout:
// [0] generations the governor currently trusts
// [1] generations actually planned for the last source frame
// [2] measured source (real) frames per second
// [3] measured presented frames per second
// [4] thermal status, -1 when the device gives no signal
// [5] GPU milliseconds the chain spends per generated frame, -1 unknown
void frameGenStats(float out[6]) const;
// Why the selected native frame-gen engine cannot run here, for the UI:
// -1 not known yet (caps not probed), 0 fine, 1 this driver lacks what the
// engine needs (lsfgCaps().reason says which gate), 2 the engine failed to start.
int frameGenProblem() const;
// Path to the SPIR-V cache built from the user's Lossless.dll. Setting it
// drops any existing engine so the next armed frame rebuilds from it.
void setLsfgCachePath(const char* path);
// Flow scale (0.25-1.0) and the panel's real refresh rate. The pacer never
// generates above the refresh rate.
// Experimental: `captureHeight` sizes the composite ring the chain runs on,
// the width following the swapchain's aspect, so a phone GPU generates at
// game-like resolution and the result is blitted up. 0 = panel;
// kFgCaptureGame = the X screen's height (containerHeight), which already
// carries the per-game override and render scale; otherwise a pixel height.
static constexpr int32_t kFgCaptureGame = -1;
void setFrameGenTuning(float flowScale, float refreshHz, int32_t captureHeight = 0);
// Which native engine generates: 0 = LSFG (needs the cache built from the
// user's Lossless.dll), 1 = win-fg (our own chain, embedded, needs nothing).
// Switching drops the other engine so only one ever holds GPU resources.
void setFrameGenEngine(int kind);
// win-fg only: interpolation model (3/4) and performance preset (0..2).
void setWinFgTuning(int model, int perfPreset);
private:
@@ -359,11 +312,9 @@ private:
ANativeWindow* window;
int surfaceWidth, surfaceHeight, containerWidth, containerHeight;
// #413 compositor clip rect (surface px). clipRegionW<=0 => disabled (whole swapchain). Written from
// the renderer thread (setClipRegion), read from the render thread (recordCmdBuf) -> atomic.
std::atomic<int> clipRegionX{0}, clipRegionY{0}, clipRegionW{0}, clipRegionH{0};
void* adrenotoolsHandle = nullptr;
bool lsfgVk11Compat_ = false; // see the constructor
bool lsfgVk11Compat_ = false;
int filterMode = 0;
bool swapRB = false;
float maxAnisotropy = 1.0f;
@@ -386,9 +337,6 @@ private:
std::vector<VkImageMemoryBarrier> framePreUpload;
std::vector<VkImageMemoryBarrier> framePostUpload;
// [P0] All SurfaceControl / AHardwareBuffer scanout state moved to
// ScanoutContext (member `scanout` above). Dead members (lastDst*,
// ScanoutPending/scanoutPending*, fnSTSetBackPressure) were dropped.
std::atomic<int> pointerX{0}, pointerY{0};
float sceneOffsetX=0.f, sceneOffsetY=0.f, sceneScaleX=1.f, sceneScaleY=1.f;
@@ -428,138 +376,69 @@ private:
VkPipeline pipeline = VK_NULL_HANDLE;
// ---- Spatial upscaler (SGSR / FSR1) ----
// upscalerMode enum (mirrored by JNI nativeSetUpscaler):
// 0 = none (passthrough; current sampler governs scaling)
// 1 = linear (rebuild base sampler LINEAR; no shader pass)
// 2 = nearest (rebuild base sampler NEAREST; no shader pass)
// 3 = sgsr (Snapdragon GSR 1.0; single pass; aspect-fit / letterbox)
// 4 = fsr (AMD FSR1 EASU+RCAS; two passes; fill / stretch)
// 5 = fsr_fit (AMD FSR1 EASU+RCAS; two passes; aspect-fit / letterbox)
// 6 = sharpen (RCAS-only; any resolution; aspect-fit / letterbox)
// 7 = nis (NVIDIA Image Scaling NVScaler; single pass; aspect-fit)
// 8 = sgsr_quality (SGSR 1 edge-direction variant; single pass; aspect-fit)
// Shader upscaling only engages for modes 3-8 AND when the game render
// resolution (container) is smaller than the swapchain. Otherwise the
// existing direct-to-swapchain path is used unchanged.
int upscalerMode = 0;
// Independent of upscalerMode: high-quality Lanczos downscale for the
// supersampling case (game render res > display res). When enabled and the
// render res exceeds the swapchain, replaces the bilinear minify with a
// proper downscale pass. Supersampling and the upscale modes are mutually
// exclusive in practice (one is render>display, the other render<display).
bool hqDownscale = false;
// Composable post effects, independent of the scaling mode. CAS layers a sharpen
// on top of any mode (incl. native res); HDR is a binary fake-HDR. The upscaler
// sharpness (RCAS lobe scale / SGSR EdgeSharpness) is driven by its own slider as a
// linear 0..1 value: 0 = neutral (no sharpening; the upscale still runs), 1 = max.
bool casEnabled = false;
int casSharpness = 60; // slider 0..100 -> CAS SHARPNESS
int casSharpness = 60;
bool hdrEnabled = false;
float upscaleSharpness01 = 0.75f; // slider/100; RCAS lobe scale, SGSR edge derived
// Debanding: terminal TPDF/IGN dither pass before the 8-bit swapchain quantize.
float upscaleSharpness01 = 0.75f;
bool debandEnabled = false;
int debandStrength = 100; // slider 0..200 -> strength/100 LSBs
int debandStrength = 100;
// Phase 2 screen effects (GL EffectComposer parity). FXAA/Toon/CRT/NTSC are
// binary; Color is the brightness/contrast/gamma grade (always-applied via the
// sliders, no toggle) and is "enabled" only when not at neutral (0,0,1).
bool fxaaEnabled = false;
bool toonEnabled = false;
bool crtEnabled = false;
bool ntscEnabled = false;
bool colorEnabled = false; // derived: !neutral grade
float colorBrightness = 0.0f; // [-1,1] (slider/100, clamped)
float colorContrast = 0.0f; // [0,2] (slider/100, clamped)
float colorGamma = 1.0f; // [0.1,5]
float colorSaturation = 1.0f; // [0,2] (slider/100, clamped)
uint32_t ntscFrameCounter = 0; // animates NTSC chroma phase
bool colorEnabled = false;
float colorBrightness = 0.0f;
float colorContrast = 0.0f;
float colorGamma = 1.0f;
float colorSaturation = 1.0f;
uint32_t ntscFrameCounter = 0;
VkSampler upscaleSampler = VK_NULL_HANDLE; // linear clamp; offscreen/mid input
VkSampler upscaleSampler = VK_NULL_HANDLE;
// === Native LSFG: composite target ring ===================================
// Frame generation cannot composite straight into a swapchain image: the
// finished frame has to be READABLE (it becomes the next frame's LSFG
// input) and generated frames have to be STORAGE-WRITABLE by a compute
// dispatch. Android swapchain images are COLOR_ATTACHMENT only, and
// storage support on a swapchain format is not something a driver owes us.
//
// So when frame gen is armed the whole existing recording is redirected at
// a composite image we own - format-identical to the swapchain, so every
// existing pipeline stays render-pass compatible - and a copy moves it into
// the acquired swapchain image at the end. With frame gen off, not one of
// these objects is created and the direct-to-swapchain path is untouched.
struct CompositeTarget {
VkImage img = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE; // colour attachment + sampled
VkImageView storageView = VK_NULL_HANDLE; // compute writes (generate)
VkImageView view = VK_NULL_HANDLE;
VkImageView storageView = VK_NULL_HANDLE;
VkFramebuffer fb = VK_NULL_HANDLE;
VkDescriptorSet ds = VK_NULL_HANDLE; // sampled, for later passes
VkDescriptorSet ds = VK_NULL_HANDLE;
};
// Hard ceiling: (max generations + 1) presentable frames per source frame,
// times a queue depth of 2, capped so the footprint stays bounded.
static constexpr uint32_t kMaxCompositeTargets = 7;
std::vector<CompositeTarget> compositeTargets;
VkRenderPass compositeRenderPass = VK_NULL_HANDLE; // CLEAR -> GENERAL
VkRenderPass compositeRenderPass = VK_NULL_HANDLE;
uint32_t compositeW = 0, compositeH = 0;
uint32_t compositeIndex = 0; // rotates per composite; gives history for free
bool compositeArmed = false; // targets exist AND this frame uses them
bool swapchainTransferDst = false; // swapchain was created with TRANSFER_DST
uint32_t compositeIndex = 0;
bool compositeArmed = false;
bool swapchainTransferDst = false;
// Set from the app when the native LSFG engine is selected for this
// session. Read on the render thread; false keeps every path as it was.
std::atomic<bool> fgArmed_{false};
std::atomic<int> fgMultiplier_{0};
// Tuning is written by the UI thread and consumed by the render thread, so
// it is published through atomics and applied to the engine at the top of
// the frame-gen block. The UI thread never touches the engine itself.
std::atomic<float> fgFlowScale_{1.0f};
std::atomic<float> fgRefreshHz_{0.0f};
std::atomic<bool> fgConfigDirty_{true};
// Experimental (see setFrameGenTuning): capture height of the composite
// ring. 0 = panel resolution, kFgCaptureGame = the X screen's height.
std::atomic<int32_t> fgCaptureHeight_{0};
// Extent the composite ring should have for the current swapchain and
// capture height (width follows the swapchain aspect, both even).
void compositeExtentFor(uint32_t& w, uint32_t& h) const;
// The extent the scene is rendered at this frame: the composite ring while
// frame gen runs on a ring smaller than the swapchain (experimental capture
// resolution), otherwise the swapchain. Every full-target pass (the effect
// chain, the spatial upscalers, the render-scale downscale) sizes itself by
// this, so those keep working below panel resolution and their
// intermediates shrink with the ring.
VkExtent2D renderExtent() const {
if (compositeActive() && (compositeW != swapchainExt.width || compositeH != swapchainExt.height))
return VkExtent2D{compositeW, compositeH};
return swapchainExt;
}
// Copy (same extent) or blit (composite smaller than the swapchain) a
// composite-ring image, already in TRANSFER_SRC, into a swapchain image
// already in TRANSFER_DST.
void recordCompositeToSwapchainTransfer(VkCommandBuffer cb, VkImage src, uint32_t imgIdx);
bool createCompositeRenderPass();
bool ensureCompositeTargets(uint32_t w, uint32_t h, uint32_t count);
void destroyCompositeTargets();
// True when this frame should composite off-swapchain. Call on the render
// thread; every gate must hold or we fall back to the untouched path.
bool compositeActive() const;
// Render pass / framebuffer this frame draws its FINAL pass into.
VkRenderPass targetRenderPass() const;
VkFramebuffer targetFramebuffer(uint32_t imgIdx) const;
// Copy the finished composite into the acquired swapchain image and leave
// it in PRESENT_SRC. No-op when the composite path is not active.
void copyCompositeToSwapchain(VkCommandBuffer cb, uint32_t imgIdx);
// === Native LSFG: the software cursor ====================================
// LSFG interpolates whatever it is given, so a cursor composited into the
// frame gets warped along the flow field and smears. It is therefore
// excluded from the composite while frame gen is armed and drawn once into
// EVERY presented image instead - real and generated alike - through a
// load-op render pass that leaves the image ready to present.
VkRenderPass cursorOverlayRenderPass = VK_NULL_HANDLE;
struct CursorOverlay {
bool draw = false;
@@ -569,19 +448,16 @@ private:
CursorOverlay cursorOverlay_{};
bool createCursorOverlayRenderPass();
// Draw the cursor into an already-copied swapchain image and leave it in
// PRESENT_SRC. Takes over the final transition from the copy helpers.
void recordCursorOverlay(VkCommandBuffer cb, uint32_t imgIdx);
// True when the cursor is being drawn per present rather than composited.
bool cursorDrawnPerPresent() const;
// === Native LSFG: the per-source-frame present plan =======================
// Interpolation produces frames that belong BETWEEN N-1 and N, so the
// generated frames are presented FIRST and real frame N is held back one
// slot. All presents for one source frame are queued together: with FIFO
// the driver then shows them on consecutive vblanks, so the pacing falls
// out of the present mode for free - no sleeps, no render-mode change.
static constexpr uint32_t kMaxPresentsPerFrame = 4; // 1 real + up to 3 generated
static constexpr uint32_t kMaxPresentsPerFrame = 4;
struct FrameGenPlan {
uint32_t generations = 0;
uint32_t presents = 1;
@@ -594,30 +470,17 @@ private:
bool lsfgEngineTried_ = false;
std::unique_ptr<winfg::Engine> winfgEngine_;
bool winfgEngineTried_ = false;
std::atomic<int> fgEngineKind_{0}; // 0 = lsfg, 1 = win-fg
std::atomic<int> fgEngineKind_{0};
std::atomic<int> fgModel_{4};
std::atomic<int> fgPerfPreset_{1};
bool ensureWinFgEngine();
// Capability gate for the SELECTED engine. win-fg's shaders need only a
// storage-capable swapchain format; the LSFG chain also needs the fp16 /
// memory-model feature set, which some otherwise capable GPUs lack.
bool fgCapsOk() const;
// Sync objects are indexed per PRESENT, not per composite: each pending
// present needs its own image-available and render-finished semaphore.
uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
// Destroy and rebuild every acquire/present semaphore. Called with the
// swapchain, so no stale pending signal can survive into the new one.
void recreateSyncObjects();
uint32_t fgAcquireFailLog_ = 0;
// Measured PRESENTS per second - the rate that actually reaches the panel,
// counting generated frames. The pacer's own "loop rate" cannot serve this
// purpose: it is sampled once per SOURCE frame, so it always equals the
// guest rate and contains no evidence that generation happened at all.
float fgPresentedRate_ = 0.0f;
// Measured SOURCE frames per second over the same window, for engines that
// do not measure it themselves (win-fg).
float fgSourceRate_ = 0.0f;
uint32_t fgSourceAccum_ = 0;
uint32_t fgPresentAccum_ = 0;
@@ -626,22 +489,10 @@ private:
void trackPresentedRate(uint32_t presents);
bool ensureLsfgEngine();
// One command buffer per pending present: slot 0 carries the composite,
// the chain's shared passes and generated frame 0; each later generated
// frame and the real frame are recorded and submitted on their own, so a
// finished frame reaches the presentation engine without waiting for the
// rest of the chain.
uint32_t cmdSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
// Shared chain passes (the 24 shaders every generated frame depends on).
void recordFrameGenProcess(VkCommandBuffer cb);
// Generated frame g: synthesise into a spare composite target and copy it
// into the swapchain image reserved for it.
void recordFrameGenGeneration(VkCommandBuffer cb, uint32_t g);
// Chain cost: a timestamp pair per frame slot, start before the shared
// passes, end after the last generation's compute (before its copy, so a
// vblank wait on the swapchain image is not counted). Read back after the
// slot's fence wait, one frame later.
VkQueryPool fgQueryPool_ = VK_NULL_HANDLE;
bool fgTimestampsOk_ = false;
float fgTimestampPeriodNs_ = 0.0f;
@@ -654,22 +505,21 @@ private:
void readFgQueryResult();
VkFormat offscreenFmt = VK_FORMAT_R8G8B8A8_UNORM;
VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; // CLEAR -> SHADER_READ_ONLY
VkRenderPass offscreenRenderPass = VK_NULL_HANDLE;
VkPipelineLayout postPipeLayout = VK_NULL_HANDLE;
VkPipeline sgsrPipeline = VK_NULL_HANDLE;
VkPipeline nisPipeline = VK_NULL_HANDLE; // NVIDIA Image Scaling (mode 7)
VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; // SGSR 1 edge-direction (mode 8)
VkPipeline nisPipeline = VK_NULL_HANDLE;
VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE;
VkPipeline easuPipeline = VK_NULL_HANDLE;
VkPipeline rcasPipeline = VK_NULL_HANDLE;
VkPipeline downscalePipeline = VK_NULL_HANDLE;
// CAS/HDR can be either a non-final pass (writes an fx target via
// offscreenRenderPass) or the final pass (writes the swapchain via renderPass);
// a pipeline is render-pass-specific, so keep one variant of each.
VkPipeline casPipelineOff = VK_NULL_HANDLE; // -> fx target (offscreenRenderPass)
VkPipeline casPipelineSwap = VK_NULL_HANDLE; // -> swapchain (renderPass)
VkPipeline casPipelineOff = VK_NULL_HANDLE;
VkPipeline casPipelineSwap = VK_NULL_HANDLE;
VkPipeline hdrPipelineOff = VK_NULL_HANDLE;
VkPipeline hdrPipelineSwap = VK_NULL_HANDLE;
// Phase 2 effects: same Off (-> fx target) / Swap (-> swapchain) variant pair.
VkPipeline fxaaPipelineOff = VK_NULL_HANDLE;
VkPipeline fxaaPipelineSwap = VK_NULL_HANDLE;
VkPipeline toonPipelineOff = VK_NULL_HANDLE;
@@ -680,10 +530,8 @@ private:
VkPipeline ntscPipelineSwap = VK_NULL_HANDLE;
VkPipeline crtPipelineOff = VK_NULL_HANDLE;
VkPipeline crtPipelineSwap = VK_NULL_HANDLE;
// Debanding is always the LAST effect (terminal) -> only a swapchain variant.
VkPipeline debandPipelineSwap = VK_NULL_HANDLE;
// offscreen composite target @ game/container resolution
VkImage offscreenImg = VK_NULL_HANDLE;
VkDeviceMemory offscreenMem = VK_NULL_HANDLE;
VkImageView offscreenView = VK_NULL_HANDLE;
@@ -691,7 +539,6 @@ private:
VkDescriptorSet offscreenDS = VK_NULL_HANDLE;
int offscreenW = 0, offscreenH = 0;
// FSR intermediate (EASU output) @ upscale output resolution
VkImage midImg = VK_NULL_HANDLE;
VkDeviceMemory midMem = VK_NULL_HANDLE;
VkImageView midView = VK_NULL_HANDLE;
@@ -699,8 +546,6 @@ private:
VkDescriptorSet midDS = VK_NULL_HANDLE;
int midW = 0, midH = 0;
// Effect-chain intermediates @ swapchain resolution. fx1 holds the scaled (or
// composited) image fed into CAS/HDR; fx2 ping-pongs when both effects run.
VkImage fx1Img = VK_NULL_HANDLE;
VkDeviceMemory fx1Mem = VK_NULL_HANDLE;
VkImageView fx1View = VK_NULL_HANDLE;
@@ -715,21 +560,18 @@ private:
VkDescriptorSet fx2DS = VK_NULL_HANDLE;
int fx2W = 0, fx2H = 0;
// Per-frame upscale plan, computed in renderFrame, consumed by recordCmdBuf.
// upFrame.mode reuses the upscalerMode enum (3=sgsr,4=fsr,5=fsr_fit,6=sharpen,7=nis,8=sgsr_quality)
// plus an internal sentinel (UPMODE_DOWNSCALE) for the supersampling path.
struct UpscaleFrame {
bool active = false;
int mode = 0;
int outX = 0, outY = 0, outW = 0, outH = 0;
bool cas = false; // run CAS this frame (snapshot of casEnabled)
bool hdr = false; // run HDR this frame (snapshot of hdrEnabled)
bool fxaa = false; // snapshot of fxaaEnabled
bool toon = false; // snapshot of toonEnabled
bool color = false; // snapshot of colorEnabled (non-neutral grade)
bool ntsc = false; // snapshot of ntscEnabled
bool crt = false; // snapshot of crtEnabled
bool deband = false;// snapshot of debandEnabled (terminal dither)
bool cas = false;
bool hdr = false;
bool fxaa = false;
bool toon = false;
bool color = false;
bool ntsc = false;
bool crt = false;
bool deband = false;
SgsrPushConstants sgsrPC{};
NisPushConstants nisPC{};
EasuPushConstants easuPC{};
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_alpha.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_beta.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_chain.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_common.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_delta.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
+17 -41
View File
@@ -1,9 +1,5 @@
// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless
// Scaling frame-generation chain.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
// by @maxjivi05. GPL-3.0-or-later.
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#include "lsfg_dll.h"
#include "lsfg_dxbc.h"
@@ -25,9 +21,9 @@
namespace lsfg {
namespace {
// ---- PE constants ---------------------------------------------------------
constexpr uint16_t kDosMagic = 0x5A4Du; // "MZ"
constexpr uint32_t kPeSignature = 0x00004550u; // "PE\0\0"
constexpr uint16_t kDosMagic = 0x5A4Du;
constexpr uint32_t kPeSignature = 0x00004550u;
constexpr uint16_t kPe32Magic = 0x010Bu;
constexpr uint16_t kPe32PlusMagic = 0x020Bu;
@@ -47,7 +43,6 @@ constexpr size_t kResourceEntrySize = 8u;
constexpr uint32_t kResourceSubdirectoryBit = 0x80000000u;
constexpr uint32_t kResourceTypeRcData = 10u;
// ---- SPIR-V constants -----------------------------------------------------
constexpr uint32_t kSpirvMagic = 0x07230203u;
constexpr size_t kSpirvHeaderWords = 5u;
constexpr uint32_t kSpirvWordCountShift = 16u;
@@ -64,7 +59,7 @@ constexpr uint32_t kVariantFp32Offset = 98u;
constexpr uint32_t kMaxResourceId = 512u;
constexpr size_t kMaxSpirvWords = 16u * 1024u * 1024u;
constexpr uint32_t kCacheMagic = 0x4C534642u; // "BFSL" little-endian
constexpr uint32_t kCacheMagic = 0x4C534642u;
constexpr uint32_t kCacheVersion = 1u;
struct CacheHeader {
@@ -83,7 +78,6 @@ struct PeSection {
uint32_t rawSize = 0;
};
// Read-only mapping of the DLL. Closes itself; the DLL is never executed.
class PeImage {
public:
~PeImage() { close(); }
@@ -168,7 +162,6 @@ private:
std::vector<PeSection> sections_;
};
// RCDATA id -> blob, for the ids we care about.
struct ResourceTable {
const uint8_t* data[kMaxResourceId] = {};
uint32_t size[kMaxResourceId] = {};
@@ -217,7 +210,6 @@ bool resourceReadLeaf(const PeImage& img, size_t leafOffset,
return true;
}
// Walk type -> name -> language and keep the first language of every RCDATA id.
bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) {
size_t typeTotal = 0;
if (!resourceEntryCount(img, resourceBase, typeTotal)) return false;
@@ -256,7 +248,7 @@ bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out)
if (!resourceReadLeaf(img, resourceBase + langOffset, blob, blobSize)) continue;
out.data[nameId] = blob;
out.size[nameId] = blobSize;
break; // first language wins
break;
}
}
}
@@ -289,7 +281,6 @@ DllStatus parseResources(PeImage& img, ResourceTable& table) {
return DllStatus::Ok;
}
// ---- SPIR-V adoption (precompiled-variant path) ---------------------------
bool isSpirvModule(const uint8_t* blob, uint32_t size) {
if (!blob || size < kSpirvHeaderWords * sizeof(uint32_t)) return false;
@@ -315,9 +306,6 @@ uint32_t lookupDescriptorSet(const std::vector<uint32_t>& w, uint32_t targetId)
return 0;
}
// Precompiled blobs are renumbered into set/binding order - that is the
// convention they were built with. The DXBC path uses encounter order instead
// (see lsfg_dxbc), which is what DXVK's output pairs with. Do not merge them.
bool renumberBindingsSetOrder(std::vector<uint32_t>& w) {
struct Slot { uint32_t set; uint32_t binding; size_t literalOffset; };
std::vector<Slot> slots;
@@ -399,8 +387,6 @@ bool writeCache(const std::string& cachePath, const CacheHeader& header, const M
if (ok) ok = fsync(fileno(f)) == 0;
fclose(f);
// Temp file + rename, so a failed or interrupted build can never leave a
// half-written cache that would later be loaded as if it were complete.
if (!ok || rename(tempPath.c_str(), cachePath.c_str()) != 0) {
unlink(tempPath.c_str());
return false;
@@ -416,9 +402,8 @@ bool readCacheHeader(const std::string& cachePath, CacheHeader& out) {
return ok && out.magic == kCacheMagic && out.version == kCacheVersion;
}
} // namespace
}
// ---- public API -----------------------------------------------------------
const std::vector<uint32_t>& shaderIds() {
static const std::vector<uint32_t> ids = [] {
@@ -584,7 +569,6 @@ DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet) {
return status;
}
// ---- SPIR-V version lowering (Vulkan 1.1/1.2 compat) ----------------------
namespace {
@@ -593,14 +577,14 @@ constexpr uint32_t kSpirvOpExtInstImport= 11u;
constexpr uint32_t kSpirvOpMemoryModel = 14u;
constexpr uint32_t kSpirvOpCapability = 17u;
// Capabilities that are core in the version the translator emits but not in
// the versions we lower to.
constexpr uint32_t kCapVulkanMemoryModel = 5345u; // core from 1.5
constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; // core from 1.5
constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; // core from 1.6
constexpr uint32_t kCapVulkanMemoryModel = 5345u;
constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u;
constexpr uint32_t kCapDemoteToHelperInvocation = 5379u;
std::vector<uint32_t> encodeOpExtension(const char* name) {
const size_t len = strlen(name) + 1; // incl. NUL
const size_t len = strlen(name) + 1;
const size_t strWords = (len + 3) / 4;
std::vector<uint32_t> ins(1 + strWords, 0u);
ins[0] = ((uint32_t)(1 + strWords) << kSpirvWordCountShift) | kSpirvOpExtension;
@@ -624,17 +608,13 @@ bool hasOpExtension(const std::vector<uint32_t>& w, const char* name) {
return false;
}
} // namespace
}
bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
if (w.size() < kSpirvHeaderWords || w[0] != kSpirvMagic) return false;
if (targetVersion < kSpirv14) return false;
if (w[1] <= targetVersion) return true; // already low enough
if (w[1] <= targetVersion) return true;
// Walk the preamble first and only touch the module once it is known to
// be lowerable: note which extension-gated capabilities are declared and
// where the capability block ends (OpExtension must follow every
// OpCapability and precede OpExtInstImport / OpMemoryModel).
bool needMemoryModel = false;
size_t insertAt = kSpirvHeaderWords;
size_t off = kSpirvHeaderWords;
@@ -645,9 +625,6 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
if (opcode == kSpirvOpCapability && length >= 2) {
const uint32_t cap = w[off + 1];
if (cap == kCapVulkanMemoryModel || cap == kCapVulkanMemoryModelDeviceScope) needMemoryModel = true;
// Core only in 1.6, and VK_EXT_shader_demote_to_helper_invocation
// is never enabled on the compat path: refuse rather than hand the
// driver a module that is not legal at the lowered version.
if (cap == kCapDemoteToHelperInvocation) return false;
insertAt = off + length;
} else if (opcode == kSpirvOpExtension) {
@@ -659,7 +636,6 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
off += length;
}
// 1.5 has the memory model as core; only 1.4 needs it declared.
if (targetVersion < kSpirv15 && needMemoryModel
&& !hasOpExtension(w, "SPV_KHR_vulkan_memory_model")) {
const auto e = encodeOpExtension("SPV_KHR_vulkan_memory_model");
@@ -669,4 +645,4 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
return true;
}
} // namespace lsfg
}
+13 -70
View File
@@ -1,35 +1,7 @@
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#pragma once
// ============================================================================
// lsfg_dll - extract the Lossless Scaling frame-generation shader chain from
// the user's own Lossless.dll, and cache it on device as SPIR-V.
//
// The 25 compute shaders that make up LSFG 3.1 are NOT redistributable. They
// ship as RCDATA resources inside Lossless.dll from the user's own paid copy
// of Lossless Scaling (Steam app 993090). Every implementation - upstream
// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their
// own DLL and extracts the resources on device. DroidDeck does the same, and
// bundles nothing.
//
// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a
// library and never executed.
//
// Resource layout, measured against real installs:
// * The base chain lives at RCDATA ids 255, 256 and 280-302 (25 modules),
// and is DXBC. It has always been DXBC.
// * Lossless Scaling MAY additionally carry precompiled SPIR-V copies at
// base+49 (native fp16) and base+98 (native fp32). No build currently
// downloadable from Steam carries them - public buildId 19655272 was
// measured with zero SPIR-V anywhere in the 311 MB install - so the DXBC
// path is the normal path and the translator is not optional. The SPIR-V
// path is kept because it costs nothing and a future build may ship them.
//
// Both producers fill the same shader-id -> SPIR-V words map, so everything
// downstream of this file is source-agnostic.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
// by @maxjivi05. GPL-3.0-or-later.
// ============================================================================
#include <cstdint>
#include <string>
@@ -39,44 +11,37 @@ namespace lsfg {
enum class DllStatus {
Ok = 0,
NotInstalled, // no such file / cannot open
UnreadableFile, // opens but cannot be mapped or is empty
NotInstalled,
UnreadableFile,
NotPortableExecutable,
MissingShaders, // parses, but the base chain is not all there
TranslationFailed, // a shader failed DXBC->SPIR-V or SPIR-V adoption
CacheUnusable // cache could not be written or read back
MissingShaders,
TranslationFailed,
CacheUnusable
};
// SPIR-V version words. The translator emits 1.6; a Vulkan 1.1 device takes
// 1.4 (via VK_KHR_spirv_1_4) and a 1.2 device 1.5. One definition for the
// probe, the engine and the shader loader.
constexpr uint32_t kSpirv14 = 0x00010400u;
constexpr uint32_t kSpirv15 = 0x00010500u;
constexpr uint32_t kSpirv16 = 0x00010600u;
enum class Variant {
None = 0,
SpirvFp16, // precompiled fp16 blobs at base+49
SpirvFp32, // precompiled fp32 blobs at base+98
DxbcTranslated // base chain, translated on device (the normal case)
SpirvFp16,
SpirvFp32,
DxbcTranslated
};
// The 25 shader ids that make up the chain.
constexpr uint32_t kShaderMipmaps = 255u;
constexpr uint32_t kShaderGenerate = 256u;
constexpr uint32_t kShaderChainFirst= 280u;
constexpr uint32_t kShaderChainLast = 302u;
constexpr uint32_t kShaderCount = 25u;
// Upstream spelling for the two ids the ported chain files refer to by name.
// Kept so those files stay byte-close to their upstream form; these alias the
// constants above rather than being a second source of truth.
constexpr uint32_t LSFG_SHADER_MIPMAPS = kShaderMipmaps;
constexpr uint32_t LSFG_SHADER_GENERATE = kShaderGenerate;
struct Module {
uint32_t id = 0;
std::vector<uint32_t> words; // SPIR-V
std::vector<uint32_t> words;
};
struct ModuleSet {
@@ -87,41 +52,19 @@ struct ModuleSet {
bool complete() const;
};
// The canonical shader-id list, in dispatch-independent order.
const std::vector<uint32_t>& shaderIds();
const char* statusName(DllStatus s);
const char* variantName(Variant v);
// Does this file look like a usable Lossless.dll? Checks the base chain only,
// so a DLL without the SPIR-V variants (i.e. every build on Steam today) is
// reported as valid - the translator handles it.
DllStatus validateDll(const std::string& dllPath);
// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath
// (via temp file + rename, so a failed build cannot leave a half-written
// cache behind). Records which producer ran in the cache header.
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16);
// Load a previously built cache.
DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet);
// Which producer built an existing cache, without loading the modules.
DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant);
// Experimental Vulkan 1.1 / 1.2 compat: lower a DXBC-translated module's
// declared SPIR-V version to `targetVersion` (kSpirv14 or kSpirv15). Below 1.5
// the Vulkan memory model is not core, so SPV_KHR_vulkan_memory_model is
// declared for it (the device side enables VK_KHR_vulkan_memory_model). The
// cache is built without a device in hand, always at the translator's native
// 1.6, so this runs at load time against whatever the device can take. A
// module already at or below the target is left untouched. Returns false,
// leaving the module untouched, for a malformed module, a target below 1.4, or
// a module declaring DemoteToHelperInvocation: that capability is core only in
// 1.6 and its extension is never enabled on the compat path, so such a module
// cannot be made legal here (DXVK emits it for pixel-shader discard only, so
// the 25 compute shaders never carry it). Whether the device then accepts the
// result is vkCreateShaderModule's / vkCreateComputePipelines' verdict.
bool downgradeSpirv(std::vector<uint32_t>& words, uint32_t targetVersion);
} // namespace lsfg
}
+5 -16
View File
@@ -1,10 +1,5 @@
// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator.
//
// Follows upstream lsfg-vk's src/extract/trans.cpp, including its
// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and
// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator
// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed,
// vendored under cpp/thirdparty/dxbc.
// Ported to WinNative and DroidDeck by @maxjivi05 from Camille LaVey / the Eden Emulator Project (GPL-3.0-or-later).
// DXVK's translator is zlib licensed; vendored under cpp/thirdparty/dxbc.
#include "lsfg_dxbc.h"
@@ -21,12 +16,12 @@ namespace {
struct BindingSlot {
uint32_t bindingIndex = 0;
uint32_t bindingOffset = 0; // word offset of the Binding literal
uint32_t bindingOffset = 0;
uint32_t setIndex = 0;
uint32_t setOffset = 0;
};
} // namespace
}
bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>& outWords) {
outWords.clear();
@@ -38,9 +33,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
const dxvk::DxbcModuleInfo info{};
auto code = module.compile(info, "CS");
// Collect the descriptor decorations in the order the Binding
// decorations appear. Everything before the first OpFunction is the
// decoration block, so the scan stops there.
std::vector<BindingSlot> slotsByVarId;
std::vector<uint32_t> varIdsInOrder;
for (auto ins : code) {
@@ -60,7 +52,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
if (ins.opCode() == spv::OpFunction) break;
}
// Renumber to a dense 0..n range, in encounter order.
uint32_t next = 0;
for (const uint32_t varId : varIdsInOrder) {
const BindingSlot& slot = slotsByVarId[varId];
@@ -75,11 +66,9 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
std::memcpy(outWords.data(), code.data(), byteSize);
return true;
} catch (...) {
// DXVK's compiler signals malformed bytecode by throwing. A bad
// resource must degrade to "this DLL is unusable", never to a crash.
outWords.clear();
return false;
}
}
} // namespace lsfg
}
+2 -30
View File
@@ -1,39 +1,11 @@
#pragma once
// ============================================================================
// lsfg_dxbc - DXBC -> SPIR-V for the Lossless Scaling shader chain.
//
// Why this exists at all: the base chain inside Lossless.dll is DXBC, and has
// always been DXBC. Lossless Scaling 3.2.2's release note ("Added shaders
// intended for use by the lsfg-vk project") describes precompiled SPIR-V
// copies at RCDATA base+49 / base+98 - but no build downloadable from Steam
// carries them. Measured against the current public build (buildId 19655272,
// 2025-08-19; linux_testing byte-identical on depot 993091): the whole 311 MB
// install contains zero occurrences of the SPIR-V magic word, and the DLL's
// 202 RCDATA entries are all DXBC.
//
// So a translator is required, not optional. Upstream lsfg-vk reaches the same
// conclusion and links DXVK's `dxbc` in src/extract/trans.cpp; we vendor the
// same subset (zlib licence) under cpp/thirdparty/dxbc and follow its
// trans.cpp step for step.
//
// The binding renumber here is ENCOUNTER ORDER - the order in which Binding
// decorations appear in DXVK's output - which is what DXVK's own layout pairs
// with. The precompiled-SPIR-V path in lsfg_dll.cpp uses a set/binding sort
// instead, because those blobs were built with that convention. The two must
// not be merged.
// ============================================================================
// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc.
#include <cstdint>
#include <vector>
namespace lsfg {
// Translate one DXBC compute shader to SPIR-V, renumbering descriptor bindings
// into a dense 0..n range in encounter order. Returns false (leaving outWords
// empty) on any malformed input - DXVK's compiler throws, and everything is
// caught here.
// Emits DXVK's native SPIR-V 1.6; lowering for a Vulkan 1.1/1.2 device happens
// at load time (lsfg_dll.h downgradeSpirv).
bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>& outWords);
} // namespace lsfg
}
+2 -43
View File
@@ -1,8 +1,3 @@
// See lsfg_engine.h.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
// @maxjivi05, where it runs on gamescope's output in the compositor.
#include "lsfg_engine.h"
@@ -21,9 +16,6 @@
namespace lsfg {
namespace {
// The chain needs two real frames in its history before anything it produces
// is meaningful, and we require the plan to stay warm for a couple of frames
// so a single settled sample cannot start generation from garbage.
constexpr uint64_t kRequiredFrames = 2;
constexpr uint32_t kRecurrenceFrames = 2;
constexpr uint64_t kTelemetryInterval = 120;
@@ -50,8 +42,6 @@ VkImageMemoryBarrier makeTransition(VkImage image, VkAccessFlags srcAccess,
return b;
}
// Copy the just-composited frame into the chain's input ring. `source` is the
// composite target, which the composite render pass leaves in GENERAL.
void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destination,
VkExtent2D extent) {
const VkImageMemoryBarrier before[] = {
@@ -88,7 +78,7 @@ void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destinat
destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL);
}
} // namespace
}
Engine::Engine() = default;
@@ -142,9 +132,6 @@ void Engine::setGuestExtent(uint32_t width, uint32_t height) {
peakGuestExtent_.height = std::max(peakGuestExtent_.height, height);
}
// The flow pyramid is the expensive part, so its resolution is scaled relative
// to the largest guest surface seen: a game running well below panel
// resolution does not need a panel-resolution flow field.
float Engine::effectiveFlowScale(uint32_t width) const {
if (width == 0 || peakGuestExtent_.width == 0) return flowScale_;
const float ratio = (float)peakGuestExtent_.width / (float)width;
@@ -171,8 +158,6 @@ bool Engine::prepare(uint32_t width, uint32_t height, VkFormat format) {
chain_ = std::make_unique<LsfgChain>(device_, *shaders_, VkExtent2D{width, height}, format,
scale);
if (!chain_->Valid()) {
// A device that cannot build the chain reports unsupported once and
// stops trying, rather than thrashing allocations every frame.
LSFG_LOGW("chain build failed at %ux%u; frame generation unavailable", width, height);
chain_.reset();
unavailable_ = true;
@@ -204,17 +189,9 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) {
plan_ = pacer_.Plan(std::min<size_t>(capacity, kMaxGenerations), sourceFrames);
// The pacer says how many frames FIT in the panel's budget. The governor
// says how many this device can actually afford right now - on a handheld
// the chain competes with the game for one GPU, so an extra generation has
// to prove it improves total output without collapsing the real frame rate.
if (governorEnabled_) {
const LsfgPacerStats s = pacer_.Stats();
governor_.configure((uint32_t)std::min<size_t>(pacer_.MaxGenerations(), kMaxGenerations));
// presentedRate_, NOT the pacer's loop rate. The pacer samples its loop
// once per SOURCE frame, so its "loop rate" is the guest rate by
// construction and can never show that generation added anything - the
// governor could not accept a probe on any hardware.
plan_.generations = governor_.cap((uint32_t)plan_.generations, s.source_rate,
presentedRate_);
}
@@ -223,12 +200,6 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) {
warmStreak_ = warm_ ? warmStreak_ + 1 : 0;
generating_ = warm_ && warmStreak_ >= kRecurrenceFrames && plan_.generations > 0;
// The ring is only seeded while generating, so the first frame after any
// gap - arming, or a pacer discontinuity after a hitch - would interpolate
// between this frame and whatever the other slot held when generation last
// stopped. That is a visible warp, and it happens every time the engine
// starts. Spend this frame filling the ring instead and generate from the
// next one, when both slots are genuinely consecutive.
primeHistory_ = false;
if (generating_ && !historyFresh()) {
primeHistory_ = true;
@@ -265,11 +236,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
lastCount_ = count;
lastGenerations_ = generations;
// Seeding the input ring is a full-resolution image copy every frame, so it
// is NOT free at 100 fps - it was costing real frame rate while producing
// nothing. Copy only when generating, on the one priming frame that
// precedes generation, or while the governor is warming up towards a
// probe - so the ring is populated by the time it is needed.
const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame()
|| (governorEnabled_ && governor_.wantsHistory());
if (needHistory) {
@@ -278,13 +244,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
haveCopied_ = true;
}
// The SHARED chain is 24 of the 25 shaders - the whole flow pyramid - and
// only `generate` runs per generated frame. Running it while producing
// nothing spends almost the entire cost of frame generation for no frames
// at all, every frame. That is what pinned the GPU at 100% on device
// regardless of the game's own settings, and it fed straight back into the
// governor: the chain made the source rate collapse, the governor saw the
// collapse and refused to generate, and refusing did not stop the chain.
if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count);
}
@@ -318,4 +277,4 @@ void Engine::reset() {
plan_ = {};
}
} // namespace lsfg
}
+2 -58
View File
@@ -1,25 +1,4 @@
#pragma once
// ============================================================================
// lsfg_engine - the compositor's handle on native LSFG frame generation.
//
// Owns the shader modules, the interpolation chain and the pacer, and exposes
// the small contract the render loop needs:
//
// prepare(w, h, format) build/rebuild the chain for this size
// plan(capacity, sourceFrames) how many frames to generate this time
// process(cmd, source, w, h, gens) take frame N in, run the shared chain
// generateInto(cmd, g, i, img, view) synthesise generated frame g
//
// Ordering matters and is not obvious: interpolation produces frames that
// belong BETWEEN N-1 and N, so the generated frames are presented FIRST and
// the real frame N is held back one slot. That one output interval is the
// unavoidable cost of interpolating rather than extrapolating; no placement of
// this code changes it.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
// @maxjivi05, where it runs on gamescope's output in the compositor.
// ============================================================================
#include <cstdint>
#include <memory>
@@ -28,7 +7,7 @@
#include <vulkan/vulkan.h>
#include "lsfg_common.hpp"
#include "lsfg_dll.h" // kSpirv16
#include "lsfg_dll.h"
#include "lsfg_governor.h"
#include "lsfg_pacer.hpp"
@@ -37,27 +16,16 @@ namespace lsfg {
class LsfgShaders;
class LsfgChain;
// Hard ceiling on generated frames per source frame (2x..4x -> 1..3).
constexpr uint32_t kMaxGenerations = 3;
class Engine {
public:
// Both the constructor and destructor are out-of-line ON PURPOSE. The
// shader set and the chain are held by unique_ptr to forward-declared
// types, and an inline constructor would make the compiler emit the
// exception-cleanup path for those members at every construction site -
// which needs the complete types there. Keeping both here confines that to
// lsfg_engine.cpp, where the types are complete.
Engine();
~Engine();
Engine(const Engine&) = delete;
Engine& operator=(const Engine&) = delete;
// Build the shader modules from a cache produced by lsfg_dll::buildCache.
// Returns false if the cache does not yield all 25 modules. `spirvTarget`
// is the highest SPIR-V version the device accepts (lsfg_probe.h); the
// cached 1.6 modules are lowered to it when it is below that.
bool init(VkDevice device, VkPhysicalDevice physicalDevice, const std::string& cachePath,
uint32_t spirvTarget = kSpirv16);
@@ -71,28 +39,18 @@ public:
bool needsRebuild(uint32_t width, uint32_t height, VkFormat format) const;
bool prepare(uint32_t width, uint32_t height, VkFormat format);
// How many frames to generate for this source frame. Returns 0 until the
// history slots are valid and the rates have settled, so the first frames
// after a start or a resize are never interpolated from garbage.
uint32_t plan(uint32_t capacity, uint64_t sourceFrames);
// Take the just-composited frame as LSFG input and run the shared part of
// the chain (everything except `generate`). `source` must be in GENERAL.
void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height,
uint32_t generations);
// Synthesise generated frame `generation` into the given storage image.
void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex,
VkImage targetImage, VkImageView targetView,
uint32_t width, uint32_t height);
// What the probe governor currently trusts, and the latest thermal
// reading (-1 = no signal). Surfaced for the HUD and diagnostics.
uint32_t acceptedGenerations() const { return governor_.accepted(); }
int thermalStatus() const { return governor_.thermalStatus(); }
float sourceRate() const;
// The rate that actually reaches the panel, generated frames included.
// Supplied by the renderer, which is the only thing that counts presents.
float loopRate() const { return presentedRate_; }
void setPresentedRate(float fps) { presentedRate_ = fps; }
@@ -102,16 +60,8 @@ public:
private:
float effectiveFlowScale(uint32_t width) const;
// A generated frame is a blend of the last two REAL frames, so the input
// ring - which is two images deep - only holds usable history when the
// other slot was filled by the frame immediately before this one. It is
// not, every time generation resumes after a pause: the ring is seeded
// only while generating, so the slot still carries whatever was there when
// generation last stopped.
bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; }
// With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the
// shared chain have to follow every frame it keeps warm, not just the ones that generate.
bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; }
Device device_{};
@@ -120,12 +70,6 @@ private:
std::unique_ptr<LsfgChain> chain_;
LsfgPacer pacer_;
ProbeGovernor governor_;
// The governor is OFF by default: the device's own thermal management is
// the authority, and a probe loop that can veto the user's setting is not
// wanted here. The multiplier is then what was asked for, still clamped by
// what the panel can show. Kept behind a flag rather than deleted because
// the probe/backoff logic is the right answer for a device WITHOUT decent
// native throttling, and this is the only thing that would have to change.
bool governorEnabled_ = false;
LsfgPlan plan_{};
@@ -151,4 +95,4 @@ private:
bool unavailable_{};
};
} // namespace lsfg
}
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_gamma.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_generate.hpp"
#include "lsfg_dll.h"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -1,4 +1,3 @@
// See lsfg_governor.h.
#include "lsfg_governor.h"
@@ -12,13 +11,10 @@
namespace lsfg {
namespace {
// A probe has to run long enough to survive a single unlucky frame, and the
// baseline long enough to be a fair comparison.
constexpr float kBaselineSeconds = 2.0f;
constexpr float kProbeSeconds = 2.0f;
constexpr uint32_t kMinSamples = 30;
// Keep the extra generation only if it actually paid off.
constexpr float kOutputGainRequired = 1.15f;
constexpr float kSourceFloor = 0.70f;
@@ -28,20 +24,20 @@ constexpr uint32_t kBackoffSteps = 4;
constexpr float kThermalPollSeconds = 2.0f;
constexpr float kThermalDecaySeconds = 1.0f;
// ADEVICE_THERMAL_STATUS_* from <android/thermal.h>, which is API 30. Resolved
// by dlsym so the build keeps working below that and on devices that do not
// ship the service; -1 simply means "no thermal signal", never "hot".
// Android reports a STATUS, not a temperature - NONE 0, LIGHT 1, MODERATE 2,
// SEVERE 3, CRITICAL 4, EMERGENCY 5, SHUTDOWN 6 - so there is no degree value
// here to raise. The equivalent of "let it run hotter" is to act on a higher
// status, which is what these now do.
//
// A handheld under a real 3D load sits at SEVERE routinely; blocking there
// made the governor refuse on a device that was working exactly as intended.
// It now only stops GROWING at SEVERE and only gives frames back at CRITICAL,
// leaving the device's own thermal management as the authority.
constexpr int kThermalBlock = 4; // CRITICAL: give generations back
constexpr int kThermalNoGrowth = 3; // SEVERE: keep what we have, stop probing
constexpr int kThermalBlock = 4;
constexpr int kThermalNoGrowth = 3;
using PFN_AThermal_acquireManager = void* (*)();
using PFN_AThermal_getCurrentThermalStatus = int (*)(void*);
@@ -72,17 +68,12 @@ float phaseSeconds(std::chrono::steady_clock::time_point a,
return std::chrono::duration<float>(b - a).count();
}
} // namespace
}
void ProbeGovernor::configure(uint32_t maxGenerations) {
if (maxGenerations_ == maxGenerations) return;
// Raising the ceiling must NOT throw away what we already know works -
// going 2x -> 4x used to reset the accepted level to zero and restart the
// whole baseline/probe cycle, so a user raising the multiplier got FEWER
// frames. Only clamp down to a ceiling that has actually been lowered.
maxGenerations_ = maxGenerations;
if (accepted_ > maxGenerations_) accepted_ = maxGenerations_;
// Re-measure against the new ceiling, but keep the accepted level.
phase_ = Phase::Baseline;
havePhaseStart_ = false;
sourceAccum_ = loopAccum_ = 0.0f;
@@ -132,19 +123,8 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
if (!havePhaseStart_) enterBaseline(now);
pollThermal(now);
// Thermal override: at SEVERE and above we stop asking for more and give
// generations back, but SLOWLY.
//
// This runs once per source frame - about forty times a second - so the
// previous version's unconditional accepted_-- was not "give one back", it
// was "slam to zero in three frames", and its unconditional enterBackoff()
// reset the backoff clock every single call, so the backoff could never
// expire and the governor could never measure or recover. One decay per
// second, and no churn at all once there is nothing left to give back.
thermalBlocked_ = thermal_ >= kThermalBlock;
if (thermal_ >= kThermalBlock) {
// Logged, sparingly. This is a REFUSAL TO EVEN MEASURE, and it was
// silent: on device it looked identical to "measured and declined".
if ((refusalLog_++ % 600u) == 0u)
GOV_LOGI("thermal status %d - not probing (accepted=%u)", thermal_, accepted_);
const bool decayDue = !haveThermalDecay_
@@ -183,12 +163,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
sourceAccum_ = loopAccum_ = 0.0f;
samples_ = 0;
} else {
// Also previously silent. "Baseline completed but we did not
// probe" is a decision worth seeing in a log.
if ((refusalLog_++ % 600u) == 0u)
GOV_LOGI("not probing: accepted=%u max=%u requested=%u thermal=%d",
accepted_, maxGenerations_, requested, thermal_);
enterBaseline(now); // nothing to try; measure again later
enterBaseline(now);
}
}
break;
@@ -226,12 +204,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
break;
}
// While probing we allow one more than is currently accepted; otherwise the
// accepted level is the ceiling, and the pacer's own answer still applies.
const uint32_t allowed = phase_ == Phase::Probing
? std::min(accepted_ + 1, maxGenerations_)
: accepted_;
return std::min(requested, allowed);
}
} // namespace lsfg
}
+1 -35
View File
@@ -1,29 +1,4 @@
#pragma once
// ============================================================================
// lsfg_governor - decides whether an extra generated frame is actually WORTH
// it on this device, right now.
//
// The ported pacer answers "how many frames fit in the panel's budget". That
// is necessary but not sufficient on a handheld: the LSFG chain competes with
// the game for the same GPU, so a naive fixed 3x on a GPU-bound title lowers
// the real frame rate by more than the generated frames add. The user sees a
// higher HUD number and a worse-looking game.
//
// So the multiplier is treated as a ceiling to earn, not a setting to obey.
// The governor starts at 0 generations, periodically PROBES one more, and
// keeps it only if the probe measurably paid off:
//
// * total output rate must improve by at least 1.15x, and
// * the real (source) frame rate must not collapse below 0.70x baseline.
//
// A failed probe reverts immediately and backs off 5s, 15s, 30s, then 60s, so
// a device that cannot afford another generation stops being asked.
//
// Thermals are the second input a phone needs and a desktop does not.
// Sustained frame generation is a thermal decision as much as a performance
// one, so the governor gives back generations as the device heats and refuses
// to probe while it is throttling.
// ============================================================================
#include <chrono>
#include <cstdint>
@@ -34,22 +9,15 @@ class ProbeGovernor {
public:
void configure(uint32_t maxGenerations);
// Cap the pacer's answer. Called once per source frame with the measured
// rates; returns the number of generations actually permitted.
uint32_t cap(uint32_t requested, float sourceRate, float loopRate);
void reset();
uint32_t accepted() const { return accepted_; }
bool probing() const { return phase_ == Phase::Probing; }
// Whether the chain's input ring needs to be kept fed. Seeding it is a
// full-resolution copy every frame, so it is skipped during the long idle
// stretches - a backoff, or a thermal block - and resumed during Baseline,
// which gives it a couple of seconds to fill before a probe can start.
bool wantsHistory() const {
return !thermalBlocked_ && phase_ != Phase::Backoff;
}
// Latest thermal reading, 0 (none) .. 6 (shutdown); -1 when unavailable.
int thermalStatus() const { return thermal_; }
private:
@@ -68,11 +36,9 @@ private:
Clock::time_point phaseStart_{};
bool havePhaseStart_ = false;
// Rolling means over the current phase window.
float sourceAccum_ = 0.0f, loopAccum_ = 0.0f;
uint32_t samples_ = 0;
// What the accepted level achieves, measured during Baseline.
float baselineSource_ = 0.0f, baselineLoop_ = 0.0f;
bool haveBaseline_ = false;
@@ -87,4 +53,4 @@ private:
bool haveThermalPoll_ = false;
};
} // namespace lsfg
}
+1 -6
View File
@@ -1,8 +1,3 @@
// JNI surface for native LSFG shader-cache management.
//
// Deliberately free of any renderer handle: extracting the shader chain from
// Lossless.dll and caching it is a slow, once-per-DLL operation that the app
// runs off the render thread, long before a game launches.
#include "lsfg_dll.h"
@@ -20,7 +15,7 @@ std::string toStdString(JNIEnv* env, jstring s) {
return out;
}
} // namespace
}
extern "C" JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_mipmaps.hpp"
#include "lsfg_dll.h"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_pacer.hpp"
@@ -21,7 +17,6 @@ constexpr float SOURCE_SMOOTHING = 0.15f;
constexpr float SOURCE_STALE_SECONDS = 0.5f;
constexpr float DISCONTINUITY_SECONDS = 0.25f;
constexpr float HEADROOM_EPSILON = 0.02f;
// A target is aimed just under, so rounding never puts output past the panel's refresh rate.
constexpr float TARGET_MARGIN = 0.1f;
constexpr float CREDIT_EPSILON = 1.0e-4f;
constexpr float SOURCE_ACCUM_FLOOR = 0.01f;
@@ -121,17 +116,6 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) {
if (target_rate > 0.0f) target_rate -= TARGET_MARGIN;
if (target_rate == 0.0f) {
// No explicit target rate: the multiplier is the multiplier, full stop.
// The panel-headroom clamp was here briefly and was removed at the
// user's decision; this is also upstream's behaviour on this branch.
//
// What that means above the panel's refresh rate, for the record: the
// display cannot show more frames than it refreshes, so under FIFO the
// surplus presents queue and the render thread waits on acquire. Nothing
// is displayed faster, and the compositor falls behind on guest
// deliveries instead. Below the refresh rate - every source at or under
// panel/4 - this branch behaves identically with or without the clamp.
// To restore it: limit = std::min(ceiling, HeadroomLimit()).
output_credit = 0.0f;
limit = ceiling;
return LsfgPlan{limit, true};
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
+3 -20
View File
@@ -1,4 +1,3 @@
// See lsfg_probe.h. Capability gate for native compositor-side LSFG.
#include "lsfg_probe.h"
#include "../VulkanRendererContext.h"
@@ -16,7 +15,7 @@ bool hasExtension(const std::vector<VkExtensionProperties>& exts, const char* na
return false;
}
} // namespace
}
FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const std::vector<VkExtensionProperties>& deviceExtensions,
@@ -33,33 +32,21 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const bool below12 = props.apiVersion < VK_API_VERSION_1_2;
if (below12) {
// Below 1.2 there is no VkPhysicalDeviceVulkan12Features to chain, and
// the SPIR-V 1.6 modules will not load anyway. Unless the experimental
// compat path is on AND the driver offers the extensions that make the
// chain loadable as SPIR-V 1.4, don't touch the device further:
// device creation stays exactly as it is today.
if (!allowVk11) return fs;
fs.hasSpirv14 = hasExtension(deviceExtensions, VK_KHR_SPIRV_1_4_EXTENSION_NAME);
fs.hasFloatControls = hasExtension(deviceExtensions, VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
fs.hasMemoryModelExt = hasExtension(deviceExtensions, VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME);
if (!fs.hasSpirv14 || !fs.hasFloatControls || !fs.hasMemoryModelExt) {
// Report what was looked at so explain() can name the gap.
fs.queried = true;
return fs;
}
fs.extensionPath = true;
fs.spirvTarget = kSpirv14;
} else if (!fs.apiAtLeast13 && allowVk11) {
// 1.2: SPIR-V 1.5 is core, VulkanMemoryModel is core. Nothing to
// enable beyond the features; the modules only need their header
// lowered from 1.6 to 1.5. Same experimental gate as the 1.1 path so
// a production build keeps rejecting 1.2 exactly as before.
fs.extensionPath = true;
fs.spirvTarget = kSpirv15;
}
// On 1.2+ the Vulkan12Features struct carries the memory model; below
// that the KHR struct does (same sType value, same layout).
VkPhysicalDeviceVulkan12Features v12{};
v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES;
VkPhysicalDeviceVulkanMemoryModelFeaturesKHR mm{};
@@ -87,7 +74,7 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
std::vector<const char*> extensionNames(const FeatureSupport& f) {
std::vector<const char*> out;
if (!f.extensionPath || f.deviceApiVersion >= VK_API_VERSION_1_2) return out;
out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); // dependency of spirv_1_4
out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
out.push_back(VK_KHR_SPIRV_1_4_EXTENSION_NAME);
out.push_back(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME);
return out;
@@ -108,10 +95,6 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) {
VkFormatProperties fp{};
vk.GetPhysicalDeviceFormatProperties(pd, fmt, &fp);
// The composite target is COLOR_ATTACHMENT (effect chain writes it),
// SAMPLED (next frame's LSFG input reads it), STORAGE (generate writes it)
// and both blit ends (it is copied - or, below panel resolution, blitted -
// into the swapchain image).
const VkFormatFeatureFlags need =
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT
| VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT
@@ -163,4 +146,4 @@ void explain(Caps& caps) {
}
}
} // namespace lsfg
}
+7 -59
View File
@@ -1,45 +1,17 @@
#pragma once
// ============================================================================
// lsfg_probe - capability gate for native (compositor-side) LSFG frame
// generation.
//
// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator
// emits as SPIR-V 1.6 with OpCapability VulkanMemoryModel and
// StorageImageWriteWithoutFormat. Three consequences, all checked here:
//
// * SPIR-V 1.6 will not load on a Vulkan 1.1 device, so the DEVICE (not just
// the instance) must report 1.3+ ... OR, on the experimental Vulkan 1.1
// compat path, offer VK_KHR_spirv_1_4 + VK_KHR_vulkan_memory_model so the
// same modules can be handed over as SPIR-V 1.4 (see downgradeSpirv in
// lsfg_dll.h). The stock Qualcomm driver on Adreno 7xx phones reports
// 1.1.128 yet carries every one of those extensions (measured on an
// Adreno 710 / HyperOS device with `cmd gpu vkjson`).
// * vulkanMemoryModel, shaderStorageImageWriteWithoutFormat and
// shaderStorageImageExtendedFormats must be ENABLED at device creation.
// Today the renderer enables no features at all, so all three are off.
// * `generate` writes into a storage image, and Android swapchain formats
// are frequently not storage-capable - so the format is probed separately
// once the swapchain has picked one.
//
// A device failing any gate reports unsupported UP FRONT, with a reason, so
// the UI can grey the engine out instead of failing later inside
// vkCreateShaderModule or vkCreateComputePipelines.
// ============================================================================
#include <vulkan/vulkan.h>
#include <cstdint>
#include <vector>
#include "lsfg_dll.h" // kSpirv14/15/16
#include "lsfg_dll.h"
struct VkTable;
namespace lsfg {
// Which of the three required features the physical device OFFERS. Queried
// before vkCreateDevice; what we actually enable is recorded in Caps below.
struct FeatureSupport {
bool queried = false; // vkGetPhysicalDeviceFeatures2 resolved
bool queried = false;
bool apiAtLeast13 = false;
bool vulkanMemoryModel = false;
bool vulkanMemoryModelDeviceScope= false;
@@ -47,65 +19,41 @@ struct FeatureSupport {
bool storageImageExtendedFormats = false;
uint32_t deviceApiVersion = 0;
// Experimental Vulkan 1.1 compat: the device is below 1.3 but offers the
// extensions that make the chain loadable. When set, the renderer must
// enable those extensions at device creation (see extensionNames) and the
// modules are downgraded to `spirvTarget` at load.
bool extensionPath = false;
bool hasSpirv14 = false; // VK_KHR_spirv_1_4
bool hasFloatControls = false; // VK_KHR_shader_float_controls (spirv_1_4 depends on it)
bool hasMemoryModelExt = false; // VK_KHR_vulkan_memory_model
bool hasSpirv14 = false;
bool hasFloatControls = false;
bool hasMemoryModelExt = false;
uint32_t spirvTarget = kSpirv16;
// Every hard device-level gate passes (format is checked separately).
bool deviceGatesPass() const {
return queried && (apiAtLeast13 || extensionPath) && vulkanMemoryModel
&& storageImageWriteWithoutFormat && storageImageExtendedFormats;
}
};
// What was actually enabled + the running verdict. Owned by the renderer.
struct Caps {
FeatureSupport features;
bool featuresEnabled = false; // the chain was passed to vkCreateDevice
bool featuresEnabled = false;
bool storageOnSwapchainFormat = false;
// The swapchain format blits with VK_FILTER_LINEAR (needs
// SAMPLED_IMAGE_FILTER_LINEAR on the source). Not a gate: without it the
// capture-resolution upscale falls back to NEAREST.
bool linearBlitOnSwapchainFormat = false;
VkFormat probedFormat = VK_FORMAT_UNDEFINED;
char reason[160] = "not probed";
// The single question the UI and the render path ask.
bool supported() const {
return featuresEnabled && features.deviceGatesPass() && storageOnSwapchainFormat;
}
};
// Ask the physical device which of the required features it offers.
// Safe on any driver: if vkGetPhysicalDeviceFeatures2 cannot be resolved, or
// the device reports below Vulkan 1.2 and `allowVk11` is false, nothing is
// chained and `queried` is left false - the caller then creates the device
// exactly as it always has. With `allowVk11`, a 1.1 device is accepted when
// `deviceExtensions` lists VK_KHR_spirv_1_4, VK_KHR_shader_float_controls and
// VK_KHR_vulkan_memory_model, and the memory-model features are queried
// through the KHR struct instead of VkPhysicalDeviceVulkan12Features.
FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const std::vector<VkExtensionProperties>& deviceExtensions,
bool allowVk11);
// The device extensions the extension path needs enabled, in the order they
// should be pushed onto VkDeviceCreateInfo. Empty unless extensionPath.
std::vector<const char*> extensionNames(const FeatureSupport& f);
// Probe VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT on the live swapchain format.
// `generate` writes into an image of this format via a compute dispatch.
bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt);
// Whether the format may be the source of a VK_FILTER_LINEAR blit.
bool probeLinearBlit(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt);
// Fill caps.reason with the FIRST gate that failed (or "supported").
void explain(Caps& caps);
} // namespace lsfg
}
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_shaders.hpp"
#include "lsfg_common.hpp"
@@ -27,12 +23,6 @@ LsfgShaders::LsfgShaders(const Device& device_, const std::string& cache_path,
return;
}
// Vulkan 1.1/1.2 compat: the cache is always built at the translator's
// native SPIR-V 1.6 (no device exists at import time), so lower each module
// to what THIS device accepts. A no-op on 1.3+ devices. Only the
// DXBC-translated chain is lowered: downgradeSpirv knows the capabilities
// DXVK emits, not whatever a precompiled vendor SPIR-V variant may carry,
// and relabelling one of those could hand the driver invalid SPIR-V.
uint32_t lowered = 0;
for (Module& module : set.modules) {
if (module.words.size() > 1 && module.words[1] > spirv_target) {
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -20,8 +16,6 @@ class Device;
class LsfgShaders {
public:
LsfgShaders() = default;
// `spirv_target` is the highest SPIR-V version the device accepts (see
// lsfg_probe.h); cached modules above it are lowered before creation.
LsfgShaders(const Device& device, const std::string& cache_path,
uint32_t spirv_target = kSpirv16);
~LsfgShaders();
@@ -1,4 +1,3 @@
// See lsfg_vkd.h. Fills the chain's dispatch from the renderer's own table.
#include "lsfg_vkd.h"
#include "../VulkanRendererContext.h"
-23
View File
@@ -1,24 +1,4 @@
#pragma once
// ============================================================================
// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global.
//
// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan
// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver
// lives in an isolated linker namespace and shares no global symbols with the
// system loader. The LSFG chain, ported from Eden by way of WinNative, calls
// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain
// code to thread a table through every call, this shim exposes exactly the 34
// entry points the chain uses and is filled from the renderer's own table when
// the chain is created.
//
// That keeps the ported files close to their upstream form, which matters:
// they are the part of this feature that is hardest to review by eye and
// easiest to break with a well-meaning edit.
//
// Single-renderer assumption: there is one Vulkan compositor per process, so
// one global table is correct. lsfgVkdInit() is idempotent and simply
// overwrites; lsfgVkdReady() reports whether every pointer resolved.
// ============================================================================
#include <vulkan/vulkan.h>
@@ -63,8 +43,5 @@ struct LsfgVkDispatch {
extern LsfgVkDispatch vkd;
// Fill `vkd` from the renderer's table. Returns false (and leaves the chain
// unusable) if any entry point failed to resolve - better to report the engine
// unsupported than to call through a null pointer mid-frame.
bool lsfgVkdInit(const VkTable& table);
bool lsfgVkdReady();
+93 -155
View File
@@ -1,6 +1,3 @@
// win-fg - runtime config: enable gate, model, multiplier, and the synthesis
// tuning parameters. Sourced from environment variables and an optional
// conf.toml (a config file, when present, wins over the env defaults).
#pragma once
#include <cstdint>
#include <cstdlib>
@@ -12,143 +9,110 @@
namespace winfg {
struct Config {
bool enabled = false; // gate; loader also honours the manifest enable var
// GRANULAR PRESENT-PATH DEBUG TRACE. Default OFF. When ON, winfg_QueuePresentKHR
// emits a per-frame, step-by-step trail (logcat tag "win-fg") so a freeze/
// crash-on-enable leaves an obvious LAST line at the exact stage that hung or
// faulted (acquire-spare, compute-submit, present-generated/real, fence wait,
// fallback). Zero behaviour change and effectively zero cost when off - one
// predicted-not-taken bool test per step, no logcat writes.
// WIN_FG_DEBUG=1|0 / conf.toml debug=on|off.
bool enabled = false;
bool debug = false;
// ── EVEN-CADENCE FRAME PACING ─────────────────────────────────────────────
// Default ON. The 2x insert path presents the GENERATED frame then the REAL
// frame back-to-back in one vkQueuePresentKHR hook call, so on Mailbox/
// Immediate both land in ~one instant followed by a full frame-interval gap
// until the next pair - uneven in time → judder ("slows down to catch up").
// With pacing on, the layer measures the real-frame interval (EMA of a
// monotonic clock) and holds the REAL present to its scheduled beat so the
// generated frame lands near the temporal MIDPOINT between consecutive real
// frames (even gen,real,gen,real cadence). Pure bounded CPU sleep - no GPU
// wait added; if a frame is already behind schedule the wait is SKIPPED
// (present immediately) so worst case is one un-paced pair, never a hitch.
// FIFO self-paces so the waits collapse to ~0 there. Off ⇒ byte-identical
// back-to-back behaviour. WIN_FG_PACING=on|off / conf.toml pacing=on|off.
// ISOLATION BUILD: default flipped to OFF to A/B the AYANEO-750 post-setting-change
// flicker regression against the pacing feature (everything else byte-identical).
bool pacing = false;
int model = 4; // 3 = symmetric flow, 4 = bidir + occlusion gate
int multiplier = 2; // generated presents per real present + 1
// EXTRA SWAPCHAIN IMAGE HEADROOM (device-freeze fix). On top of the +1 spare the
// insert path needs, request this many ADDITIONAL images at swapchain creation:
// requested minImageCount = app_min + 1 (spare) + extra_images,
// clamped to surfaceCaps.maxImageCount (0 = unlimited -> no clamp), never below
// app_min+1. The extra headroom stops the guest's NEXT vkAcquireNextImageKHR from
// starving while win-fg holds a spare + a real frame in flight to the AHB-backed
// host compositor. Some SoCs (Adreno 840 / Turnip) recycle displayed AHBs too
// slowly to tolerate the extra present with only the +1 spare and freeze on the
// first inserted frame; the AYANEO (Adreno 750) recycles fast enough to tolerate
// it. Default 2 (a few MB VRAM), applied on all devices. On a device that already
// works this only grows the pool - no FPS/quality change; the pool-headroom guard
// in QueuePresentKHR stays a no-op when headroom is adequate.
// WIN_FG_EXTRA_IMAGES / conf.toml extra_images.
int model = 4;
int multiplier = 2;
int extraImages = 2;
// ── FLOW-RESOLUTION PERFORMANCE PRESET ────────────────────────────────────
// Trades base-FPS-drop against optical-flow quality by selecting the FINEST
// pyramid level the dense flow is solved at (FrameGen::kFlowFinest):
// 0 = Quality -> kFlowFinest = 1 (~1/2-res flow; best motion, biggest FG cost)
// 1 = Balanced -> kFlowFinest = 2 (~1/4-res; TODAY's behaviour) [DEFAULT]
// 2 = Performance -> kFlowFinest = 3 (~1/8-res; cheapest -> smallest base-FPS drop)
// Out-of-range clamps to Balanced (1), so an unset/older conf.toml is byte-
// identical to today. Changing this LIVE rebuilds the affected per-size flow
// images (like a resize) inside the layer and resets the flow predictor, so a
// preset change is fully hot - no FG toggle needed. See FrameGen::configure /
// FrameGen::flowFinestForPreset. WIN_FG_PERF_PRESET / conf.toml perf_preset.
int perfPreset = 1; // 0 quality, 1 balanced (default), 2 performance
float flowScale = 1.0f; // scales solved flow magnitude
// C1 GLOBAL-MOTION PRE-WARP. Estimate the per-frame camera affine (LK) and
// remove it before the dense SAD flow search so the search only sees coherent
// object-only residual motion (kills fast-pan "melt"). 0 = auto (engage only
// when the LK solve is stable), 1 = on (force-engage once a solve exists),
// 2 = off (never pre-warp; identity affine → pipeline == pre-C1 exactly).
// WIN_FG_GM=auto|on|off / conf.toml global_motion=auto|on|off.
int gmMode = 0; // 0 auto, 1 on, 2 off
// C2 FLOW REGULARIZATION (TV-L1 smoothness prior). After C1 removes the camera
// motion, the dense flow (flowLvl_ at kFlowFinest, 1/4-res) is the OBJECT-only
// residual; it still carries spurious per-block vectors. C2 runs N semi-implicit
// TV-L1 denoising iterations on that field - an L1 data term + edge-aware Total-
// Variation regularizer - to kill incoherent vectors while KEEPING true motion
// discontinuities (object edges) sharp. Solved in-place; expand's median + C1
// add-back consume the cleaned field unchanged. 0 iterations ⇒ byte-identical to
// pre-C2. See shaders/of3_flowreg.comp.
// WIN_FG_FLOWREG=auto|on|off / conf.toml flow_reg=auto|on|off.
int frMode = 0; // 0 auto (engaged), 1 on (engaged), 2 off
int frIters = 4; // TV-L1 iterations at kFlowFinest (0 ⇒ off)
float frLambda = 2.0f; // L1 data-fidelity weight (higher ⇒ trust SAD flow)
float frDt = 0.25f; // semi-implicit smoothing step (higher ⇒ smoother)
float frEdge = 8.0f; // luma-gradient edge sensitivity for the TV weight g
float frEps = 0.05f; // Charbonnier epsilon (px) for the TV diffusivity
// synthesis (wfg_synth) tuning
float beta = 8.0f; // softmax sharpness on importance Z
float lambda = 0.6f; // FB-consistency vs photometric weight
// Tightened 2026-08-17 to prioritise "no ghost" over max sharpness - the
// remaining single-frame ghosts came from pixels that the previous defaults
// still trusted with warp when they should have cross-faded. Raising both
// pushes borderline pixels toward the safe cross-fade path (softer, but no
// smear). Prewarp will lift the ceiling further; this is the "safety net"
// knob change until then.
// Walked back from (0.10, 9.0) tighten after user reported softening. The
// real fix for the residual ghosts is the 3a-fallback path change in
// layer.cpp (present real curr on fallback instead of blitting synth over);
// the gate can stay closer to the original values now.
float epsilon = 0.07f; // disocclusion floor (0.05 -> 0.10 -> 0.07)
float photoScale = 7.5f; // photometric residual scale into Z (6.0 -> 9.0 -> 7.5)
// HUD exclusion rect in pixel coords (x0,y0,x1,y1). Fragments inside are
// passed through as the real current frame (no warp, no synth) so text /
// overlays don't ghost. Disabled when x0 >= x1 or y0 >= y1 (the default).
// Sourced from WIN_FG_HUD_RECT="x0,y0,x1,y1" env var or conf.toml
// (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen -
// host knows where the HUD is, tell the shader to skip it.
int perfPreset = 1;
float flowScale = 1.0f;
int gmMode = 0;
int frMode = 0;
int frIters = 4;
float frLambda = 2.0f;
float frDt = 0.25f;
float frEdge = 8.0f;
float frEps = 0.05f;
float beta = 8.0f;
float lambda = 0.6f;
float epsilon = 0.07f;
float photoScale = 7.5f;
float hudX0 = 0.0f;
float hudY0 = 0.0f;
float hudX1 = 0.0f;
float hudY1 = 0.0f;
// ── TRAINING-DATA CAPTURE MODE (Route-B VFI dataset collection) ───────────
// A dev-only gate. When OFF (default) the present path is byte-identical to a
// build without this feature - a single bool test and nothing else. When ON,
// every real present is downscaled on the GPU, read back async, and written to
// disk as lossless QOI (motion-rich 256² triplet patches by default, or full
// downscaled frames) plus a JSONL manifest, for offline (i-1,i+1)->i training.
// NEVER captures win-fg's generated frames - capture reads the real swapchain
// image at the TOP of QueuePresentKHR, before any interpolation.
// WIN_FG_CAPTURE=on|off / conf.toml capture=on|off.
bool capture = false;
// Downscale target box (aspect-preserving, never upscales). WIN_FG_CAPTURE_W/H,
// conf capture_width/capture_height.
int captureW = 1280;
int captureH = 720;
// 0 = patch (self-contained aligned triplets, DEFAULT), 1 = frame (full
// downscaled frames in order). WIN_FG_CAPTURE_MODE=patch|frame.
int capMode = 0;
int capPatches = 3; // patches per triplet in patch mode (WIN_FG_CAPTURE_PATCHES)
int capPatchSize = 256; // patch edge in downscaled px (WIN_FG_CAPTURE_PATCH)
// Skip a unit whose inter-frame motion (mean per-pixel luma abs-diff, 0..255)
// is below this. WIN_FG_CAPTURE_MOTION / conf capture_motion.
int capPatches = 3;
int capPatchSize = 256;
float capMotion = 2.0f;
// Output dir; empty => $HOME/.cache/winfg-capture. WIN_FG_CAPTURE_DIR /
// conf capture_dir. A per-run session subdir is created under it.
std::string capDir;
// Rolling-shard size cap (MiB). Output is a small number of large packed
// .wfgcap containers, not thousands of loose files; a new shard opens when the
// current one passes this. WIN_FG_CAPTURE_SHARD_MB / conf capture_shard_mb.
int capShardMB = 1024;
void sanitize() {
if (model < 3) model = 3; if (model > 4) model = 4;
if (multiplier < 2) multiplier = 2; if (multiplier > 4) multiplier = 4;
if (extraImages < 0) extraImages = 0; if (extraImages > 8) extraImages = 8;
if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; // clamp out-of-range -> Balanced
if (perfPreset < 0 || perfPreset > 2) perfPreset = 1;
if (gmMode < 0) gmMode = 0; if (gmMode > 2) gmMode = 2;
if (frMode < 0) frMode = 0; if (frMode > 2) frMode = 2;
if (frIters < 0) frIters = 0; if (frIters > 16) frIters = 16;
@@ -158,7 +122,6 @@ struct Config {
if (frEps < 1e-3f) frEps = 1e-3f;
if (flowScale < 0.05f) flowScale = 0.05f; if (flowScale > 4.0f) flowScale = 4.0f;
if (beta < 0.0f) beta = 0.0f; if (lambda < 0.0f) lambda = 0.0f;
// capture knobs
if (captureW < 16) captureW = 16; if (captureW > 7680) captureW = 7680;
if (captureH < 16) captureH = 16; if (captureH > 4320) captureH = 4320;
if (capMode < 0) capMode = 0; if (capMode > 1) capMode = 1;
@@ -176,7 +139,6 @@ static inline int envi(const char* k, int d) {
const char* v = std::getenv(k); return v ? std::atoi(v) : d;
}
// "auto"/"on"/"off" (also true/false/on/off numerics) -> {0 auto, 1 on, 2 off}.
static inline int parse_tristate(const std::string& v, int dflt) {
if (v.empty()) return dflt;
if (v == "auto") return 0;
@@ -185,19 +147,16 @@ static inline int parse_tristate(const std::string& v, int dflt) {
return dflt;
}
// "on"/"off"/"1"/"true"/... -> bool.
static inline bool parse_bool(const std::string& v, bool dflt) {
int t = parse_tristate(v, dflt ? 1 : 2);
return t == 1;
}
// "patch"|"frame" -> {0 patch, 1 frame}.
static inline int parse_capmode(const std::string& v, int dflt) {
if (v == "patch" || v == "patches" || v == "0") return 0;
if (v == "frame" || v == "frames" || v == "1") return 1;
return dflt;
}
// key=value TOML-lite reader (flat keys, '#' comments) - enough for our knobs.
static inline void apply_toml(Config& c, const std::string& path) {
std::ifstream f(path);
if (!f) return;
@@ -229,7 +188,6 @@ static inline void apply_toml(Config& c, const std::string& path) {
else if (k == "lambda") c.lambda = std::strtof(v.c_str(), nullptr);
else if (k == "epsilon") c.epsilon = std::strtof(v.c_str(), nullptr);
else if (k == "photoScale") c.photoScale = std::strtof(v.c_str(), nullptr);
// ── capture-mode knobs ────────────────────────────────────────────────
else if (k == "capture") c.capture = parse_bool(v, c.capture);
else if (k == "capture_dir") c.capDir = v;
else if (k == "capture_width") c.captureW = std::atoi(v.c_str());
@@ -240,7 +198,6 @@ static inline void apply_toml(Config& c, const std::string& path) {
else if (k == "capture_motion") c.capMotion = std::strtof(v.c_str(), nullptr);
else if (k == "capture_shard_mb") c.capShardMB = std::atoi(v.c_str());
else if (k == "hudRect") {
// "x0,y0,x1,y1" in pixel coords
float r[4] = {0,0,0,0}; int n = 0;
size_t start = 0; std::string s = v;
for (; n < 4; ++n) {
@@ -255,22 +212,18 @@ static inline void apply_toml(Config& c, const std::string& path) {
}
}
// Path of the conf.toml the app writes (used for hot-reload mtime checks).
static inline std::string conf_path() {
if (const char* p = std::getenv("WIN_FG_CONF")) return p;
const char* home = std::getenv("HOME");
return home ? std::string(home) + "/.config/win-fg/conf.toml" : std::string();
}
// Resolve the capture output root: explicit override, else $HOME/.cache/winfg-capture,
// else a /tmp fallback so a HOME-less prefix still writes somewhere pullable.
static inline std::string capture_root(const Config& c) {
if (!c.capDir.empty()) return c.capDir;
if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture";
return std::string("/tmp/winfg-capture");
}
// env defaults first, then conf.toml overrides (file wins when present).
static inline Config load_config() {
Config c;
c.enabled = envi("WIN_FG_ENABLE", 0) != 0;
@@ -292,7 +245,6 @@ static inline Config load_config() {
c.lambda = envf("WIN_FG_LAMBDA", c.lambda);
c.epsilon = envf("WIN_FG_EPSILON", c.epsilon);
c.photoScale = envf("WIN_FG_PHOTOSCALE", c.photoScale);
// ── capture-mode env (conf.toml still wins below) ─────────────────────────
if (const char* cc = std::getenv("WIN_FG_CAPTURE")) c.capture = parse_bool(cc, c.capture);
c.captureW = envi("WIN_FG_CAPTURE_W", c.captureW);
c.captureH = envi("WIN_FG_CAPTURE_H", c.captureH);
@@ -303,7 +255,6 @@ static inline Config load_config() {
c.capShardMB = envi("WIN_FG_CAPTURE_SHARD_MB", c.capShardMB);
if (const char* cd = std::getenv("WIN_FG_CAPTURE_DIR")) c.capDir = cd;
if (const char* h = std::getenv("WIN_FG_HUD_RECT")) {
// "x0,y0,x1,y1" - pixel coords; disabled when x0>=x1 or y0>=y1
float r[4] = {0,0,0,0}; int n = 0;
std::string s = h; size_t start = 0;
for (; n < 4; ++n) {
@@ -321,37 +272,24 @@ static inline Config load_config() {
return c;
}
// UBO layout - MUST match wfg_synth.comp binding 0.
// The vec4 hudRect is 16-byte aligned in std140, sits at offset 32 (after the
// 8 leading floats which naturally occupy 32B). Disabled when hudX0>=hudX1.
struct SynthUBO {
float flowScale, alpha, beta, lambda, epsilon, photoScale, pad0, pad1;
float hudX0, hudY0, hudX1, hudY1;
};
// UBO layout - MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO).
struct FlowUBO { float flowScale; uint32_t level; float occlLo; float occlHi; };
// C1 global-motion UBO - MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp /
// of3_expand(_m4). std140: four vec4 = 64 bytes. The affine is in centered-UV
// space: P = [[p1,p3],[p2,p4]], translation (tx,ty); warp uvSrc = uv + P*(uv-0.5)
// + t maps a prev(template) coord to the curr(image) coord it samples (prev→curr).
// lin[] = running LK estimate (linearization point fed to of3_gm_reduce)
// app[] = APPLIED affine (of3_gm_prewarp + expand add-back); identity when
// disengaged so the pipeline is byte-identical to pre-C1.
struct GMUBO {
float lin[4]; // p1,p2,p3,p4
float linT[4]; // tx,ty, engaged, pad
float app[4]; // p1,p2,p3,p4
float appT[4]; // tx,ty, pad, pad
float lin[4];
float linT[4];
float app[4];
float appT[4];
};
// C2 flow-regularization UBO - MUST match binding 0 (vec4 params) in
// of3_flowreg.comp. std140: one vec4 = 16 bytes. See Config::fr* fields.
struct FlowRegUBO {
float dt; // params.x - semi-implicit smoothing step
float lambda; // params.y - L1 data-fidelity weight (θ = dt·lambda)
float edgeAlpha; // params.z - luma-gradient edge sensitivity for g
float epsTV; // params.w - Charbonnier epsilon (px) for the TV diffusivity
float dt;
float lambda;
float edgeAlpha;
float epsTV;
};
} // namespace winfg
}
@@ -1,5 +1,3 @@
// GENERATED by tools/embed_spv.py - do not edit by hand.
// win-fg embedded SPIR-V: our optical-flow front end + synthesis only.
#pragma once
#include <cstdint>
#include <cstddef>
@@ -41,4 +39,4 @@ static inline Blob blob(Shader s) {
default: return { nullptr, 0 };
}
}
} } // namespace winfg::embedded
} }
+8 -36
View File
@@ -1,4 +1,3 @@
// win-fg - compute frame-generation engine implementation.
#include "framegen.hpp"
#include "embedded_shaders.hpp"
#include "log.hpp"
@@ -49,10 +48,6 @@ void FrameGen::destroyImage(Img& i) {
i = Img{};
}
// C1: allocate the per-slot host-visible SSBOs the LK reduce writes partials into.
// Host-visible + coherent so the CPU can read them back after the owning frame's
// fence signals (no staging copy needed; the buffers are tiny - kGmThreads*kGmStride
// floats each, ~64 KB). Persistently mapped.
bool FrameGen::makeGmBuffers() {
const VkDeviceSize sz = (VkDeviceSize)kGmThreads * kGmStride * sizeof(float);
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
@@ -75,8 +70,6 @@ bool FrameGen::makeGmBuffers() {
bool FrameGen::makePipe(Pipe& p, int shaderId,
const std::vector<VkDescriptorType>&) {
// binding layout is derived per shader below in init(); here we only build
// the module + a generic layout supplied by caller via setLayout already set.
embedded::Blob b = embedded::blob((embedded::Shader)shaderId);
VkShaderModuleCreateInfo mi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
mi.codeSize = b.words * 4; mi.pCode = b.code;
@@ -101,7 +94,6 @@ void FrameGen::destroyPipe(Pipe& p) {
p = Pipe{};
}
// Build a descriptor-set layout from an explicit (binding, type) list.
static bool buildSetLayout(const DeviceDispatch* dd, VkDevice dev,
const std::vector<std::pair<uint32_t, VkDescriptorType>>& binds,
VkDescriptorSetLayout& out) {
@@ -123,12 +115,11 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id,
si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
VKOK(dd_->CreateSampler(device_, &si, nullptr, &linear_));
// descriptor pool sized generously for our per-frame sets.
VkDescriptorPoolSize sizes[] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 256},
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 64},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16}, // C1 LK-reduce SSBOs
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16},
};
VkDescriptorPoolCreateInfo pci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
@@ -141,19 +132,15 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id,
const auto U = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
const auto SB = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
// per-shader binding sets (match the GLSL layout(binding=...) declarations)
if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pLuma_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pDown_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlow_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowM4_.setLayout)) return false;
// C1: expand sets gain a second UBO (binding 1 = GMUBO) for the global-motion add-back.
if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpand_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpandM4_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{35,S},B{48,W}}, pSynth_.setLayout)) return false;
// C1: LK-reduce (UBO, SSBO, prev, curr) and curr-luma stabilization prewarp (UBO, src, dst).
if (!buildSetLayout(dd_, dev, {B{1,U},B{16,SB},B{32,S},B{33,S}}, pGmReduce_.setLayout)) return false;
if (!buildSetLayout(dd_, dev, {B{1,U},B{32,S},B{48,W}}, pGmPrewarp_.setLayout)) return false;
// C2: flow-reg (UBO, u_in, f0_obs, luma, out).
if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowReg_.setLayout)) return false;
std::vector<VkDescriptorType> unused;
@@ -177,23 +164,19 @@ static VkExtent2D levelExtent(VkExtent2D e, int l) {
return { e.width >> l ? e.width >> l : 1u, e.height >> l ? e.height >> l : 1u };
}
bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force) {
// force skips the same-extent early-out so a perf_preset change (which alters
// flowFinest_ and therefore the per-size flow-image resolutions) rebuilds them.
bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) {
if (!force && ready_ && extent.width == extent_.width && extent.height == extent_.height) return true;
// tear down previous size
for (auto& i : pyrA_) destroyImage(i); pyrA_.clear();
for (auto& i : pyrB_) destroyImage(i); pyrB_.clear();
for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear();
for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear();
destroyImage(flowExpA_); destroyImage(flowExpB_);
destroyImage(flowRegA_); destroyImage(flowRegB_); // C2
destroyImage(flowRegA_); destroyImage(flowRegB_);
extent_ = extent; ready_ = false;
const VkImageUsageFlags lumaUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
// flowLvl_ is cleared every frame (per-frame reset) and C2 copies the cleaned
// finest level back into it - both need TRANSFER_DST. The C2 ping-pong images
// are the copy SOURCE (TRANSFER_SRC).
const VkImageUsageFlags flowUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
const VkImageUsageFlags regUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
pyrA_.resize(kLevels); pyrB_.resize(kLevels); flowLvl_.resize(kLevels);
@@ -203,13 +186,11 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force)
if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
}
// C1: stabilized curr luma - only the levels the dense flow reads (finest..coarsest).
pyrAs_.resize(kLevels);
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(flowExpA_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
if (!makeImage(flowExpB_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
// C2: TV-L1 ping-pong scratch at the finest solved flow level (perf_preset-driven res).
if (!makeImage(flowRegA_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false;
if (!makeImage(flowRegB_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false;
ready_ = true;
@@ -229,13 +210,6 @@ void FrameGen::barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkI
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
}
// NOTE: descriptor-set allocation/update per record() and the exact per-level
// flow wiring are implemented in record_impl.inc for readability; the graph is:
// luma(curr)->pyrA[0], luma(prev)->pyrB[0]
// downsample pyrA/pyrB to kLevels
// coarse->fine flow (per level) into flowLvl
// expand flowLvl[0] -> flowExpA/flowExpB
// synth(prev,curr,flowExpA,flowExpB, alpha) -> out
void FrameGen::destroy() {
if (!device_) return;
@@ -245,11 +219,11 @@ void FrameGen::destroy() {
for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear();
for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear();
destroyImage(flowExpA_); destroyImage(flowExpB_);
destroyImage(flowRegA_); destroyImage(flowRegB_); // C2
destroyImage(flowRegA_); destroyImage(flowRegB_);
destroyPipe(pLuma_); destroyPipe(pDown_); destroyPipe(pFlow_); destroyPipe(pFlowM4_);
destroyPipe(pExpand_); destroyPipe(pExpandM4_); destroyPipe(pSynth_);
destroyPipe(pGmReduce_); destroyPipe(pGmPrewarp_);
destroyPipe(pFlowReg_); // C2
destroyPipe(pFlowReg_);
for (int s = 0; s < kGmSlots; ++s) {
if (gmSsboMem_[s]) dd_->UnmapMemory(device_, gmSsboMem_[s]);
if (gmSsbo_[s]) dd_->DestroyBuffer(device_, gmSsbo_[s], nullptr);
@@ -262,8 +236,6 @@ void FrameGen::destroy() {
descPool_ = VK_NULL_HANDLE; linear_ = VK_NULL_HANDLE; device_ = VK_NULL_HANDLE; ready_ = false;
}
} // namespace winfg
}
// record() + its scratch helpers reopen namespace winfg themselves, so include
// this OUTSIDE the namespace block above (keeps FrameGen:: members attached).
#include "record_impl.inc"
+38 -79
View File
@@ -1,8 +1,3 @@
// win-fg - compute frame-generation engine.
// Owns the compute pipelines (our of3_* optical flow + wfg_synth) and the
// per-resolution intermediate images, and records the flow+synth graph that
// turns (prev, curr) into an interpolated frame. Written for win-fg; no code
// from bionic-fg / lsfg / GameScope.
#pragma once
#include "vk_dispatch.hpp"
#include "config.hpp"
@@ -14,70 +9,53 @@ class FrameGen {
public:
bool init(const DeviceDispatch* dd, const InstanceDispatch* id,
VkPhysicalDevice phys, VkDevice dev, uint32_t queueFamily, VkQueue queue);
// Applies a new config. Recomputes the active finest flow level from
// cfg_.perfPreset; if it changed while resources are live, safely rebuilds the
// affected per-size flow images (like a resize) and resets the flow predictor,
// so a perf_preset change is FULLY LIVE - no FG toggle needed. Defined in
// record_impl.inc (needs the scratch-state teardown). See flowFinestForPreset.
void configure(const Config& c);
// (Re)build per-resolution resources for a swapchain of the given size/format.
// force=true rebuilds even when the extent is unchanged (used by configure()
// when a perf_preset change alters the finest solved flow level).
bool onResize(VkExtent2D extent, VkFormat colorFormat, bool force = false);
// Record the whole flow+synth graph into cmd. prevView/currView are SAMPLED
// views of the two real frames; outView is a STORAGE view of the target
// image the generated frame is written into. alpha in (0,1).
// gmSlot selects which C1 global-motion SSBO to reduce into / read back; the
// caller MUST pass the ring-slot index of the FrameCtx it just fence-waited
// (that guarantees this slot's PREVIOUS reduce has completed and is safe to
// read on the host - see the decoupled-LK note in record_impl.inc).
void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
VkImageView outView, float alpha, uint32_t gmSlot = 0);
void destroy();
bool valid() const { return device_ != VK_NULL_HANDLE && ready_; }
const Config& config() const { return cfg_; }
// public so the record_impl.inc free helpers can reference them
struct Img { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE; VkExtent2D extent{}; VkFormat fmt{}; };
struct Pipe { VkPipeline pipeline = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE;
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; VkShaderModule module = VK_NULL_HANDLE; };
// ── C1 GLOBAL-MOTION PRE-WARP constants (public so record_impl.inc's free
// helpers can size/index the SSBO pool). See of3_gm_reduce/of3_gm_prewarp. ──
static constexpr int kGmSlots = 3; // >= present ring size; one SSBO per in-flight ctx
static constexpr int kGmLevel = 3; // 1/8-res luma pyramid level for the LK estimate
static constexpr int kGmThreads = 512; // fixed reduce threads (8 groups * local_size_x 64)
static constexpr int kGmStride = 32; // floats/thread: 21 H + 6 b + coverage + resid + pad
// Finest pyramid level the dense flow is solved at (0 = full res). This is now
// RUNTIME-SELECTABLE via conf.toml perf_preset (0 Quality=1 / 1 Balanced=2 /
// 2 Performance=3). kFlowFinestDefault is the Balanced value (today's behaviour);
// the ACTIVE level lives in flowFinest_ and is read through flowFinest(). onResize
// sizes the stabilized-curr pyramid + C2 scratch from it, and record_impl.inc
// wires the flow/expand inputs to it - both must agree, so they all read
// flowFinest()/flowFinest_ (never a hard-coded 2). (Was a static constexpr = 2.)
static constexpr int kFlowFinestDefault = 2; // Balanced (perf_preset=1)
// Map perf_preset -> finest solved level. Out-of-range -> Balanced. A higher
// level = coarser (cheaper) flow = smaller base-FPS drop. configure() clamps the
// result against kLevels so at least two solved levels always remain.
static constexpr int kGmSlots = 3;
static constexpr int kGmLevel = 3;
static constexpr int kGmThreads = 512;
static constexpr int kGmStride = 32;
static constexpr int kFlowFinestDefault = 2;
static int flowFinestForPreset(int preset) {
switch (preset) {
case 0: return 1; // Quality ~1/2-res
case 2: return 3; // Performance ~1/8-res
case 1: default: return kFlowFinestDefault; // Balanced ~1/4-res (today)
case 0: return 1;
case 2: return 3;
case 1: default: return kFlowFinestDefault;
}
}
// The ACTIVE finest solved flow level (set by configure() from perf_preset).
int flowFinest() const { return flowFinest_; }
private:
uint32_t findMemType(uint32_t bits, VkMemoryPropertyFlags props) const;
bool makeImage(Img& out, VkExtent2D ext, VkFormat fmt, VkImageUsageFlags usage);
void destroyImage(Img& i);
bool makeGmBuffers(); // allocate the kGmSlots host-visible LK-reduce SSBOs
// Read back one reduce SSBO, solve the 6x6 inverse-compositional LK step, and
// update the running/applied global affine. Returns true if it engaged this
// frame. Implemented in record_impl.inc (has the ScratchState/UBO context).
bool makeGmBuffers();
bool updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent);
bool makePipe(Pipe& p, int shaderId,
const std::vector<VkDescriptorType>& bindings);
@@ -97,55 +75,36 @@ private:
Config cfg_;
VkExtent2D extent_{};
bool ready_ = false;
// ACTIVE finest solved flow level (perf_preset). Default = Balanced (today's 2)
// so an unconfigured engine and an unset conf.toml are byte-identical to before.
int flowFinest_ = kFlowFinestDefault;
// pipelines
Pipe pLuma_, pDown_, pFlow_, pFlowM4_, pExpand_, pExpandM4_, pSynth_;
// C1: LK normal-equation reduce + curr-luma stabilization pre-warp
Pipe pGmReduce_, pGmPrewarp_;
// C2: TV-L1 flow-regularization iteration (one dispatch per iteration)
Pipe pFlowReg_;
// per-resolution resources
// 7 levels (was 5): the coarsest levels set the max motion the solver can track.
// At 5 levels the reach was only ~±84px/frame at 720p, so fast camera motion
// (150-300px/frame at the periphery) saturated the search -> incoherent vectors
// -> oil-paint melt. Two more (tiny) coarse levels push the reach to ~±300px so
// large global motion is captured coherently instead of smeared. Near-zero cost:
// the extra levels are 40x22 and 20x11 images.
static const int kLevels = 7;
std::vector<Img> pyrA_, pyrB_; // luma pyramids for curr / prev
std::vector<Img> pyrAs_; // C1: STABILIZED curr luma (flow levels only)
std::vector<Img> flowLvl_; // per-level flow scratch
Img flowExpA_, flowExpB_; // fwd / bwd expanded flow (+conf)
// C2: TV-L1 ping-pong scratch at kFlowFinest res. flowLvl_[kFlowFinest] holds
// the fixed observation f0 (never written during the iterations); u ping-pongs
// between these two; the final iterate is copied back into flowLvl_[kFlowFinest]
// so of3_expand consumes the cleaned field with no binding change (⇒ skipping
// C2 leaves flowLvl_ byte-identical to pre-C2).
std::vector<Img> pyrA_, pyrB_;
std::vector<Img> pyrAs_;
std::vector<Img> flowLvl_;
Img flowExpA_, flowExpB_;
Img flowRegA_, flowRegB_;
// ── C1 global-motion state ────────────────────────────────────────────────
// Host-visible SSBOs the reduce shader writes per-thread partials into; the
// CPU sums + solves them. One per ring slot so a readback never races a live
// submission (the caller fence-waited its slot before calling record()).
VkBuffer gmSsbo_[kGmSlots] {};
VkDeviceMemory gmSsboMem_[kGmSlots]{};
void* gmSsboPtr_[kGmSlots]{};
bool gmBuffersReady_ = false;
// running affine params p1..p6 in centered-UV space (see GMUBO in config.hpp);
// identity = all zero. gmLinAt_ records the linearization params in effect when
// each slot's reduce was dispatched (the correct IC-update base for that slot).
double gmRun_[6] {};
double gmLinAt_[kGmSlots][6] {};
bool gmSlotValid_[kGmSlots]{};
bool gmHaveEst_ = false; // at least one good solve so far
int gmStableCount_ = 0; // consecutive good solves (auto-engage threshold)
bool gmEngaged_ = false; // is the affine being APPLIED this frame
float gmLastTransPx_ = 0.0f; // estimated global translation magnitude (full-res px), for logging
bool gmHaveEst_ = false;
int gmStableCount_ = 0;
bool gmEngaged_ = false;
float gmLastTransPx_ = 0.0f;
unsigned long long gmFrames_ = 0;
};
} // namespace winfg
}
-8
View File
@@ -1,5 +1,3 @@
// win-fg - logging. On Android goes to logcat (tag "win-fg") so device bring-up
// can trace the layer; elsewhere falls back to stderr.
#pragma once
#if defined(__ANDROID__)
#include <android/log.h>
@@ -11,10 +9,4 @@
#define WFG_LOGE(...) do { std::fprintf(stderr, "[win-fg][E] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0)
#endif
// Runtime-gated granular debug trace. The gate is a RUNTIME bool (Config::debug),
// not a compile-time switch - a shipping .so carries the full trail but pays only
// a predicted-not-taken bool test per step when debug is off (no logcat write, no
// arg evaluation of the message). Same "win-fg" tag so the app captures it under
// its own UID. Used to step the present path so a freeze leaves an obvious last
// line at the stage that hung. Usage: WFG_LOGD(dbg, "P#%llu step ...", n);
#define WFG_LOGD(cond, ...) do { if (cond) WFG_LOGI(__VA_ARGS__); } while(0)
+32 -160
View File
@@ -1,57 +1,28 @@
// win-fg - FrameGen::record() and its lazy per-size descriptor/UBO setup.
// Included from framegen.cpp (has access to FrameGen privates). Written for
// win-fg; standard Vulkan compute wiring, no third-party code.
//
// ── C1 GLOBAL-MOTION PRE-WARP (this file wires it end to end) ─────────────────
// The dense SAD flow search melts on fast camera pans because the global motion
// saturates its window. C1 estimates the per-frame camera affine with a cheap
// inverse-compositional Lucas-Kanade solve (of3_gm_reduce accumulates the 6x6
// normal equations at 1/8 res → CPU 6x6 Cholesky here), then of3_gm_prewarp
// removes it from the curr luma pyramid BEFORE the search, so the search only
// sees small, coherent, object-only residual motion. of3_expand(_m4) composes
// the global affine back so wfg_synth is unchanged and the final warp is correct.
//
// DECOUPLED (stall-free) SCHEDULING: a true in-frame CPU solve would need a
// mid-frame GPU submit+readback stall - unacceptable on a GPU-bound title. So we
// read back the reduce SSBO the caller ALREADY fence-waited (its ring slot from a
// couple of presents ago) and run ONE Gauss-Newton step per frame, warm-started
// from the running estimate. Camera motion is temporally smooth, so a ~1-2 frame
// old affine is an excellent predictor; and because the SAME affine is removed
// (prewarp) and added back (expand), the result is EXACT for any affine we pick -
// an approximate/stale one only changes how much residual the search must absorb,
// never correctness. Identity affine ⇒ byte-identical to the pre-C1 pipeline, so
// the bail-to-identity safety path can never be worse than "no pre-warp".
namespace {
struct ScratchState {
// host-visible UBO buffers
VkBuffer synthUbo = VK_NULL_HANDLE; VkDeviceMemory synthUboMem = VK_NULL_HANDLE; void* synthUboPtr = nullptr;
std::vector<VkBuffer> flowUbo; std::vector<VkDeviceMemory> flowUboMem; std::vector<void*> flowUboPtr;
VkBuffer expandUbo = VK_NULL_HANDLE; VkDeviceMemory expandUboMem = VK_NULL_HANDLE; void* expandUboPtr = nullptr;
// C2: flow-regularization UBO (shared by all iterations; refreshed per frame).
VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr;
// C1: per-slot global-motion UBO (double-buffered by ring slot so a live GPU
// read is never overwritten by the next frame - see the header note).
VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{};
VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{};
void* gmUboPtr[winfg::FrameGen::kGmSlots]{};
// persistent descriptor sets
VkDescriptorSet sLumaCurr = VK_NULL_HANDLE, sLumaPrev = VK_NULL_HANDLE;
std::vector<VkDescriptorSet> sDownA, sDownB, sFlow;
VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{}; // per-slot (binds gmUbo[slot])
// C2: three flow-reg sets covering every (u_in → u_out) pairing of the
// ping-pong. sFlowReg0: in=flowLvl_[F] (=f0), out=A. sFlowRegAB: in=A, out=B.
// sFlowRegBA: in=B, out=A. f0 (binding 33) = flowLvl_[F] in all three.
VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{};
VkDescriptorSet sFlowReg0 = VK_NULL_HANDLE, sFlowRegAB = VK_NULL_HANDLE, sFlowRegBA = VK_NULL_HANDLE;
VkDescriptorSet sSynth = VK_NULL_HANDLE;
// C1: per-slot reduce set + per-slot per-level prewarp sets
VkDescriptorSet sGmReduce[winfg::FrameGen::kGmSlots]{};
std::vector<VkDescriptorSet> sGmPrewarp[winfg::FrameGen::kGmSlots]; // sized kLevels; [flowFinest()..) used
std::vector<VkDescriptorSet> sGmPrewarp[winfg::FrameGen::kGmSlots];
VkExtent2D builtFor{0,0};
bool built = false;
bool flowInited = false; // flow images laid out once, then persist (temporal predictor)
bool flowInited = false;
};
ScratchState g_scratch; // one swapchain in flight in this first-draft engine
ScratchState g_scratch;
}
static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev,
@@ -77,7 +48,6 @@ static VkDescriptorSet allocSet(const winfg::DeviceDispatch* dd, VkDevice dev,
VkDescriptorSet s = VK_NULL_HANDLE; dd->AllocateDescriptorSets(dev, &ai, &s); return s;
}
// write helpers
static void wImg(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
VkDescriptorType type, VkImageView view, VkSampler samp, VkImageLayout layout) {
VkDescriptorImageInfo ii{}; ii.imageView = view; ii.sampler = samp; ii.imageLayout = layout;
@@ -102,9 +72,6 @@ static void wSsbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet
dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr);
}
// ── C1 CPU-side linear algebra (clean-room, textbook) ────────────────────────
// 6x6 symmetric positive-definite solve via Cholesky. Returns false if not PD
// (a bad/ill-conditioned frame → caller bails to identity). Damped by the caller.
static bool cholSolve6(double A[6][6], const double b[6], double x[6]) {
double L[6][6] = {{0}};
for (int i = 0; i < 6; ++i) {
@@ -112,7 +79,7 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) {
double s = A[i][j];
for (int k = 0; k < j; ++k) s -= L[i][k] * L[j][k];
if (i == j) {
if (s <= 1e-20) return false; // not positive-definite
if (s <= 1e-20) return false;
L[i][j] = std::sqrt(s);
} else {
L[i][j] = s / L[j][j];
@@ -124,7 +91,6 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) {
for (int i = 5; i >= 0; --i) { double s = y[i]; for (int k = i+1; k < 6; ++k) s -= L[k][i]*x[k]; x[i] = s / L[i][i]; }
return true;
}
// affine <-> params. params p=(p1,p2,p3,p4,p5,p6); M = I + [[p1,p3,p5],[p2,p4,p6]].
static void affFromParams(const double p[6], double M[3][3]) {
M[0][0]=1.0+p[0]; M[0][1]=p[2]; M[0][2]=p[4];
M[1][0]=p[1]; M[1][1]=1.0+p[3]; M[1][2]=p[5];
@@ -153,8 +119,6 @@ static void mulAff(const double A[3][3], const double B[3][3], double out[3][3])
namespace winfg {
// Lazily allocate per-size UBOs + descriptor sets and wire the fixed (internal-
// resource) bindings once. External views (prev/curr/out) are wired per frame.
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool,
VkSampler samp, VkExtent2D ext,
@@ -168,11 +132,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
FrameGen::Img& flowRegA, FrameGen::Img& flowRegB, VkBuffer* gmSsbo,
int kLevels, const Config& cfg);
// Tear down the lazily-built scratch (all UBO buffers + every descriptor set) so a
// live perf_preset rebuild neither leaks them nor exhausts the descriptor pool.
// The CALLER must have made the device idle first - no in-flight command buffer may
// still reference these. Resetting the pool frees every set FrameGen allocated (all
// sets come from this pool); the mapped UBO memory is unmapped + freed explicitly.
static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorPool pool) {
auto freeUbo = [&](VkBuffer& b, VkDeviceMemory& m, void*& p) {
if (m) dd->UnmapMemory(dev, m);
@@ -186,29 +145,20 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP
freeUbo(S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
for (size_t i = 0; i < S.flowUbo.size(); ++i) freeUbo(S.flowUbo[i], S.flowUboMem[i], S.flowUboPtr[i]);
for (int s = 0; s < FrameGen::kGmSlots; ++s) freeUbo(S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]);
if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0); // reclaim all sets at once
S = ScratchState{}; // built=false -> next record() rebuilds via ensureScratch
if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0);
S = ScratchState{};
}
// ── perf_preset apply / live rebuild ─────────────────────────────────────────
// Sets the config, resolves the perf_preset -> finest-solved-level, and - when
// that level changed while resources are already live - safely rebuilds the
// affected per-size flow images (a resize) + scratch descriptor sets and resets
// the flow predictor. This makes a perf_preset change FULLY LIVE per-present (the
// conf.toml hot-reload path calls this), so no FG toggle / bg-fg cycle is needed.
void FrameGen::configure(const Config& c) {
cfg_ = c; cfg_.sanitize();
// Map preset -> finest level, clamped so at least TWO solved levels always
// remain (coarse->fine needs a range). With kLevels=7 the presets (1/2/3) never
// hit the clamp; the guard is defensive for future level/preset tuning.
int want = flowFinestForPreset(cfg_.perfPreset);
const int maxFinest = kLevels - 2; // keep >= 2 solved levels
const int maxFinest = kLevels - 2;
if (want > maxFinest) {
WFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
"(need >=2 solved) - clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
want = maxFinest;
}
if (want < 1) want = 1; // never solve full-res (level 0)
if (want < 1) want = 1;
const int prevFinest = flowFinest_;
flowFinest_ = want;
@@ -216,35 +166,27 @@ void FrameGen::configure(const Config& c) {
WFG_LOGI("perf-preset=%d -> kFlowFinest=%d (was %d) live-rebuild=%d ready=%d",
cfg_.perfPreset, flowFinest_, prevFinest, live ? 1 : 0, ready_ ? 1 : 0);
if (!live) return; // unchanged, or not yet built (initial configure before onResize)
if (!live) return;
// The per-size flow images (pyrAs_/flowReg*/flowLvl_ wiring) and the scratch
// descriptor sets were built for the OLD finest level. Rebuild them so record()
// never reads/writes mismatched-size images.
dd_->DeviceWaitIdle(device_); // no in-flight cmd buf may touch these
destroyScratch(dd_, device_, descPool_); // free old scratch (UBOs + all sets)
if (!onResize(extent_, VK_FORMAT_UNDEFINED, /*force=*/true))
dd_->DeviceWaitIdle(device_);
destroyScratch(dd_, device_, descPool_);
if (!onResize(extent_, VK_FORMAT_UNDEFINED, true))
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
extent_.width, extent_.height);
// Reset the persistent flow predictor so stale, differently-scaled state never
// feeds the new resolution (avoids a melt/garbage frame on switch). The per-frame
// flow field (flowLvl_) is already cleared every frame; the cross-frame state is
// the C1 global-motion estimate + its per-slot reduce partials (dispatched against
// the old pyramid) - drop both back to identity.
for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0;
for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; }
gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f;
WFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_);
}
// ── C1: read back one reduce SSBO, run one inverse-compositional LK step ──────
// Updates the running/applied global affine. Returns whether GM engages this frame.
bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
++gmFrames_;
if (!gmBuffersReady_ || cfg_.gmMode == 2 /*off*/) {
// off: keep identity applied; keep estimating cheaply is pointless when off.
if (!gmBuffersReady_ || cfg_.gmMode == 2 ) {
gmEngaged_ = false; gmStableCount_ = 0;
// still refresh the linearization record so a live switch to auto starts clean
for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0;
for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i];
gmLastTransPx_ = 0.0f;
@@ -260,31 +202,25 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
for (int k = 0; k < kGmStride; ++k) sums[k] += (double)row[k];
}
double coverage = sums[27];
// require a few percent of the level's pixels as inliers
double lvlpx = double((fullExtent.width >> kGmLevel) * (fullExtent.height >> kGmLevel));
double minCov = 0.03 * (lvlpx > 0 ? lvlpx : 1.0);
if (coverage > minCov && coverage > 64.0) {
double H[6][6]; int idx = 0;
for (int r = 0; r < 6; ++r) for (int c = r; c < 6; ++c) { H[r][c] = H[c][r] = sums[idx]; ++idx; }
double b[6]; for (int i = 0; i < 6; ++i) b[i] = sums[21 + i];
// Levenberg damping for conditioning (relative + tiny absolute).
for (int i = 0; i < 6; ++i) H[i][i] += 1e-3 * H[i][i] + 1e-9;
double dp[6];
if (cholSolve6(H, b, dp)) {
// reject wild steps (something non-affine / a scene cut)
bool bounded = std::fabs(dp[0]) < 0.5 && std::fabs(dp[1]) < 0.5 &&
std::fabs(dp[2]) < 0.5 && std::fabs(dp[3]) < 0.5 &&
std::fabs(dp[4]) < 0.25 && std::fabs(dp[5]) < 0.25;
if (bounded) {
// IC update: M_new = M_lin ∘ W(dp)^-1 (compose onto the SLOT's
// linearization base, which is what this reduce linearized around).
double Mlin[3][3], Md[3][3], Mdi[3][3], Mnew[3][3];
affFromParams(gmLinAt_[gmSlot], Mlin);
affFromParams(dp, Md);
if (invAff(Md, Mdi)) {
mulAff(Mlin, Mdi, Mnew);
double pnew[6]; paramsFromAff(Mnew, pnew);
// final sanity clamp on the composed estimate
bool sane = std::fabs(pnew[0]) < 0.6 && std::fabs(pnew[1]) < 0.6 &&
std::fabs(pnew[2]) < 0.6 && std::fabs(pnew[3]) < 0.6 &&
std::fabs(pnew[4]) < 0.5 && std::fabs(pnew[5]) < 0.5;
@@ -297,17 +233,14 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
if (good) ++gmStableCount_; else gmStableCount_ = 0;
// engage policy
switch (cfg_.gmMode) {
case 1: gmEngaged_ = gmHaveEst_; break; // on
case 2: gmEngaged_ = false; break; // off (unreachable; handled above)
default: gmEngaged_ = (gmStableCount_ >= 3); break; // auto: 3 consecutive good solves
case 1: gmEngaged_ = gmHaveEst_; break;
case 2: gmEngaged_ = false; break;
default: gmEngaged_ = (gmStableCount_ >= 3); break;
}
// record the linearization base for the reduce we are about to dispatch into
// this slot this frame (i.e. the current running estimate).
for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i];
gmSlotValid_[gmSlot] = true; // this frame's reduce will (re)write it
gmSlotValid_[gmSlot] = true;
gmLastTransPx_ = (float)std::sqrt(
(gmRun_[4]*fullExtent.width) * (gmRun_[4]*fullExtent.width) +
@@ -330,9 +263,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_);
}
// ── C1: solve the global-motion affine (reads the SSBO the caller fence-waited) ──
bool gmEngaged = updateGlobalMotion(gmSlot, extent_);
// Build this slot's GMUBO from the running / applied affine.
if (g_scratch.gmUboPtr[gmSlot]) {
GMUBO g{};
g.lin[0]=(float)gmRun_[0]; g.lin[1]=(float)gmRun_[1]; g.lin[2]=(float)gmRun_[2]; g.lin[3]=(float)gmRun_[3];
@@ -340,10 +271,9 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
if (gmEngaged) {
g.app[0]=(float)gmRun_[0]; g.app[1]=(float)gmRun_[1]; g.app[2]=(float)gmRun_[2]; g.app[3]=(float)gmRun_[3];
g.appT[0]=(float)gmRun_[4]; g.appT[1]=(float)gmRun_[5];
} // else app/appT stay 0 -> identity -> prewarp/expand are pre-C1 no-ops
}
std::memcpy(g_scratch.gmUboPtr[gmSlot], &g, sizeof(g));
}
// Log engage state + estimated global translation periodically (tag win-fg).
{
static int lastEng = -1;
if (gmEngaged != (lastEng == 1) || (gmFrames_ % 300ull) == 0) {
@@ -353,12 +283,10 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
}
}
// update UBOs for this frame
SynthUBO su{cfg_.flowScale, alpha, cfg_.beta, cfg_.lambda, cfg_.epsilon, cfg_.photoScale, 0, 0,
cfg_.hudX0, cfg_.hudY0, cfg_.hudX1, cfg_.hudY1};
if (g_scratch.synthUboPtr) std::memcpy(g_scratch.synthUboPtr, &su, sizeof(su));
// wire per-frame external views
wImg(dd_, device_, g_scratch.sLumaCurr, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
wImg(dd_, device_, g_scratch.sLumaPrev, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
wImg(dd_, device_, g_scratch.sSynth, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
@@ -372,23 +300,13 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
};
auto bar = [&](FrameGen::Img& i){ barrier(cmd, i.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL); };
// Internal images are created UNDEFINED and fully recomputed every frame, so
// discard-transition them all to GENERAL up front (all descriptors use GENERAL
// for these). This also fixes the first-frame layout (storage writes require
// GENERAL, not UNDEFINED).
for (auto& i : pyrA_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
for (auto& i : pyrB_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
for (int l = flowFinest_; l < kLevels; ++l) barrier(cmd, pyrAs_[l].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
barrier(cmd, flowExpA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
barrier(cmd, flowExpB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
// C2: ping-pong scratch to GENERAL (storage writes require it; contents discarded).
barrier(cmd, flowRegA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
barrier(cmd, flowRegB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
// Reset the flow field to zero every frame. The coarsest level uses it as its
// search predictor; carrying it across frames lets a spurious vector feed
// forward until it diverges and melts the image (the bg/fg pause was resetting
// it - that's why artifacts vanished on resume). Fresh zero every frame =
// stable, standard pyramid flow, no feedback loop.
{
VkClearColorValue zero{};
VkImageSubresourceRange rng{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
@@ -406,20 +324,14 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
}
}
// 1) luma of curr/prev at full res
disp(pLuma_, g_scratch.sLumaCurr, levelExtent(extent_, 0));
disp(pLuma_, g_scratch.sLumaPrev, levelExtent(extent_, 0));
bar(pyrA_[0]); bar(pyrB_[0]);
// 2) downsample pyramids
for (int l = 1; l < kLevels; ++l) {
disp(pDown_, g_scratch.sDownA[l - 1], levelExtent(extent_, l));
disp(pDown_, g_scratch.sDownB[l - 1], levelExtent(extent_, l));
bar(pyrA_[l]); bar(pyrB_[l]);
}
// 2b) C1: LK normal-equation reduce (fixed grid, 1/8-res) → this slot's SSBO.
// Reads raw prev/curr luma at kGmLevel; writes per-thread partials. A
// compute→host barrier makes the write visible to the CPU readback that
// happens when this slot comes round again (~1-2 frames later).
if (gmBuffersReady_ && g_scratch.sGmReduce[gmSlot]) {
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.pipeline);
dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.layout, 0, 1, &g_scratch.sGmReduce[gmSlot], 0, nullptr);
@@ -429,28 +341,15 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &mb, 0, nullptr, 0, nullptr);
}
// 2c) C1: pre-warp curr luma pyramid by the applied affine → stabilized pyramid
// (identity affine ⇒ exact copy, so the flow below is unchanged when
// disengaged). The dense flow reads pyrAs from here on.
for (int l = flowFinest_; l < kLevels; ++l) {
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
bar(pyrAs_[l]);
}
// 3) coarse->fine flow (only down to flowFinest_ - reduced res for perf; the
// finest level is perf_preset-selectable). Curr input is now the STABILIZED
// pyramid (pyrAs), wired in ensureScratch.
for (int l = kLevels - 1; l >= flowFinest_; --l) {
disp(flowP, g_scratch.sFlow[l], levelExtent(extent_, l));
bar(flowLvl_[l]);
}
// 3b) ── C2: TV-L1 flow regularization on the finest residual field ──────────
// Run frIters semi-implicit TV-L1 denoising iterations on flowLvl_[flowFinest_]
// (the object-only residual left by C1). flowLvl_[F] is the fixed data term f0
// (read, never written here); u ping-pongs A<->B; the final iterate is copied
// back into flowLvl_[F] so of3_expand consumes the cleaned field unchanged.
// Disengaged (frMode==off or frIters==0) ⇒ flowLvl_[F] untouched ⇒ byte-identical
// to pre-C2. Identity/zero iterations is therefore never worse than "no reg".
const bool frEngaged = (cfg_.frMode != 2) && (cfg_.frIters > 0);
if (frEngaged) {
const int F = flowFinest_;
@@ -471,9 +370,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
bar(*outImg);
last = outImg;
}
// Copy the cleaned finest flow back into flowLvl_[F] (GENERAL->GENERAL).
// Barrier: C2's f0 reads (SHADER_READ of flowLvl_[F]) and the last iterate
// write must complete before the transfer overwrites/reads.
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
mb.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
mb.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -491,7 +387,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, 1, &mb2, 0, nullptr, 0, nullptr);
}
{ // periodic C2 status (tag win-fg), mirrors the C1 log cadence
{
static int lastFr = -1;
int fe = frEngaged ? 1 : 0;
if (fe != lastFr || (gmFrames_ % 300ull) == 0) {
@@ -501,14 +397,11 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
}
}
// 4) expand finest solved flow -> fwd/bwd (+conf), + C1 global-motion add-back.
disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0));
bar(flowExpA_); bar(flowExpB_);
// 5) synthesise interpolated frame
disp(pSynth_, g_scratch.sSynth, levelExtent(extent_, 0));
}
// ---- lazy scratch builder ----
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool,
VkSampler samp, VkExtent2D ext,
@@ -528,7 +421,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
makeUbo(dd, dev, mp, sizeof(SynthUBO), S.synthUbo, S.synthUboMem, S.synthUboPtr);
makeUbo(dd, dev, mp, sizeof(FlowUBO), S.expandUbo, S.expandUboMem, S.expandUboPtr);
if (S.expandUboPtr) { FlowUBO u{cfg.flowScale, 0, 0.6f, 1.6f}; std::memcpy(S.expandUboPtr, &u, sizeof(u)); }
// C2: flow-reg UBO (params refreshed each frame in record() for hot-reload).
makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); }
S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels);
@@ -536,19 +428,16 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]);
if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); }
}
// C1: per-slot GMUBO (initialised to identity)
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
makeUbo(dd, dev, mp, sizeof(GMUBO), S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]);
if (S.gmUboPtr[s]) { GMUBO g{}; std::memcpy(S.gmUboPtr[s], &g, sizeof(g)); }
}
// luma sets (external src wired per frame; dst is pyramid[0])
S.sLumaCurr = allocSet(dd, dev, pool, pLuma.setLayout);
S.sLumaPrev = allocSet(dd, dev, pool, pLuma.setLayout);
wImg(dd, dev, S.sLumaCurr, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrA[0].view, VK_NULL_HANDLE, GEN);
wImg(dd, dev, S.sLumaPrev, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[0].view, VK_NULL_HANDLE, GEN);
// downsample sets (fixed)
S.sDownA.resize(kLevels - 1); S.sDownB.resize(kLevels - 1);
for (int l = 1; l < kLevels; ++l) {
S.sDownA[l-1] = allocSet(dd, dev, pool, pDown.setLayout);
@@ -559,10 +448,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
wImg(dd, dev, S.sDownB[l-1], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[l].view, VK_NULL_HANDLE, GEN);
}
// flow sets (fixed): predictor = coarser flow level, or self at coarsest.
// C1: the CURR input (binding 33) is the STABILIZED pyramid pyrAs for the
// solved flow levels; prev stays raw. Below kFlowFinest the level is never
// dispatched, so leaving those at raw curr is harmless.
S.sFlow.resize(kLevels);
for (int l = 0; l < kLevels; ++l) {
S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout);
@@ -575,19 +460,17 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN);
}
// C1: reduce sets (one per slot) - prev/curr raw luma at kGmLevel + SSBO + GMUBO.
if (gmSsbo) {
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
if (!gmSsbo[s]) continue;
S.sGmReduce[s] = allocSet(dd, dev, pool, pGmReduce.setLayout);
wUbo (dd, dev, S.sGmReduce[s], 1, S.gmUbo[s], sizeof(GMUBO));
wSsbo(dd, dev, S.sGmReduce[s], 16, gmSsbo[s], VK_WHOLE_SIZE);
wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN); // prev = template
wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN); // curr = image
wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN);
wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN);
}
}
// C1: prewarp sets (one per slot, per solved flow level) - raw curr in, stabilized out.
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
S.sGmPrewarp[s].assign(kLevels, VK_NULL_HANDLE);
for (int l = fg->flowFinest(); l < kLevels; ++l) {
@@ -598,7 +481,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
}
}
// expand sets (one per slot): flow inputs fixed; binding 1 = that slot's GMUBO.
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
S.sExpand[s] = allocSet(dd, dev, pool, pExpand.setLayout);
wUbo(dd, dev, S.sExpand[s], 0, S.expandUbo, sizeof(FlowUBO));
@@ -608,9 +490,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
wImg(dd, dev, S.sExpand[s], 49, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, expB.view, VK_NULL_HANDLE, GEN);
}
// C2: flow-reg sets. F = kFlowFinest. f0 (binding 33) = flowLvl_[F] in all; luma
// (binding 34) = prev luma at F (pyrB) for the edge-aware weight. u_in (32) and
// u_out (48) alternate to realize the ping-pong (see ScratchState comment).
{
const int F = fg->flowFinest();
S.sFlowReg0 = allocSet(dd, dev, pool, pFlowReg.setLayout);
@@ -619,26 +498,19 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
VkDescriptorSet all3[3] = {S.sFlowReg0, S.sFlowRegAB, S.sFlowRegBA};
for (VkDescriptorSet s : all3) {
wUbo(dd, dev, s, 0, S.flowRegUbo, sizeof(FlowRegUBO));
wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); // f0
wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN); // luma
wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN);
wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN);
}
// iter0: in=flowLvl_[F] (=f0), out=A
wImg(dd, dev, S.sFlowReg0, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN);
wImg(dd, dev, S.sFlowReg0, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN);
// odd iters: in=A, out=B
wImg(dd, dev, S.sFlowRegAB, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegA.view, samp, GEN);
wImg(dd, dev, S.sFlowRegAB, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegB.view, VK_NULL_HANDLE, GEN);
// even iters (>0): in=B, out=A
wImg(dd, dev, S.sFlowRegBA, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegB.view, samp, GEN);
wImg(dd, dev, S.sFlowRegBA, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN);
}
// synth set: external prev/curr/out wired per frame; flow inputs fixed
S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout);
wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO));
// GENERAL (not SR): these internal flow images stay in GENERAL across the whole
// record() (written as storage by expand, sampled here) - see the discard
// transition at the top of record().
wImg(dd, dev, S.sSynth, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN);
wImg(dd, dev, S.sSynth, 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN);
@@ -646,4 +518,4 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
return true;
}
} // namespace winfg
}
@@ -1,7 +1,3 @@
// win-fg - minimal Vulkan layer dispatch tables.
// Structure follows the public Khronos loader/layer interface and the
// renderdoc "Vulkan layer guide" skeleton (learning references, Apache-2.0 /
// CC-BY). No code from bionic-fg / lsfg / GameScope.
#pragma once
#include <vulkan/vulkan.h>
#include <vulkan/vk_layer.h>
@@ -9,53 +5,35 @@
namespace winfg {
// The loader sets the first pointer-sized word of a dispatchable handle to the
// dispatch table pointer; layers key their own maps on that value so that all
// handles sharing a chain resolve to the same table.
static inline void* dispatch_key(void* handle) { return *reinterpret_cast<void**>(handle); }
// Instance-level functions we call from the layer.
struct InstanceDispatch {
PFN_vkGetInstanceProcAddr GetInstanceProcAddr = nullptr;
PFN_vkDestroyInstance DestroyInstance = nullptr;
PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties = nullptr;
PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties = nullptr;
// Device-context logging (affected-device fingerprint). Properties2 is core 1.1;
// may be null on a 1.0 instance - always null-check before calling.
PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties = nullptr;
PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2 = nullptr;
// Surface caps - used to clamp the extra-image-headroom request to the surface's
// maxImageCount so the bump never exceeds what the driver can grant.
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR GetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr;
};
// Device-level functions we call. Populated by walking vkGetDeviceProcAddr on
// the next layer down at device-create time.
struct DeviceDispatch {
PFN_vkGetDeviceProcAddr GetDeviceProcAddr = nullptr;
PFN_vkDestroyDevice DestroyDevice = nullptr;
PFN_vkGetDeviceQueue GetDeviceQueue = nullptr;
PFN_vkQueueSubmit QueueSubmit = nullptr;
// Synchronization2 submit path (core 1.3 + KHR alias). DXVK may use either the
// legacy QueueSubmit or QueueSubmit2/2KHR; both are hooked for the guest-submit
// diagnostic. May be null if the device did not enable them - always null-check.
PFN_vkQueueSubmit2 QueueSubmit2 = nullptr;
PFN_vkQueueSubmit2KHR QueueSubmit2KHR = nullptr;
PFN_vkQueueWaitIdle QueueWaitIdle = nullptr;
PFN_vkDeviceWaitIdle DeviceWaitIdle = nullptr;
// swapchain / present
PFN_vkCreateSwapchainKHR CreateSwapchainKHR = nullptr;
PFN_vkDestroySwapchainKHR DestroySwapchainKHR = nullptr;
PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR = nullptr;
PFN_vkAcquireNextImageKHR AcquireNextImageKHR = nullptr;
// Alternate acquire entry (VK_KHR_swapchain 1.1+ / device-group). DXVK may use
// either AcquireNextImageKHR or AcquireNextImage2KHR - both hooked for the
// guest-acquire diagnostic. May be null - always null-check.
PFN_vkAcquireNextImage2KHR AcquireNextImage2KHR = nullptr;
PFN_vkQueuePresentKHR QueuePresentKHR = nullptr;
// resources
PFN_vkCreateImage CreateImage = nullptr;
PFN_vkDestroyImage DestroyImage = nullptr;
PFN_vkCreateImageView CreateImageView = nullptr;
@@ -73,7 +51,6 @@ struct DeviceDispatch {
PFN_vkCreateSampler CreateSampler = nullptr;
PFN_vkDestroySampler DestroySampler = nullptr;
// pipeline / descriptors
PFN_vkCreateShaderModule CreateShaderModule = nullptr;
PFN_vkDestroyShaderModule DestroyShaderModule = nullptr;
PFN_vkCreateDescriptorSetLayout CreateDescriptorSetLayout = nullptr;
@@ -84,15 +61,10 @@ struct DeviceDispatch {
PFN_vkDestroyPipeline DestroyPipeline = nullptr;
PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr;
PFN_vkDestroyDescriptorPool DestroyDescriptorPool = nullptr;
// Frees every set in the pool at once - used by FrameGen::configure() to reclaim
// the old scratch descriptor sets when a perf_preset change forces a live rebuild
// (otherwise repeated preset switches would exhaust the pool). May be null in
// theory; the rebuild path null-checks before calling.
PFN_vkResetDescriptorPool ResetDescriptorPool = nullptr;
PFN_vkAllocateDescriptorSets AllocateDescriptorSets = nullptr;
PFN_vkUpdateDescriptorSets UpdateDescriptorSets = nullptr;
// command recording
PFN_vkCreateCommandPool CreateCommandPool = nullptr;
PFN_vkDestroyCommandPool DestroyCommandPool = nullptr;
PFN_vkAllocateCommandBuffers AllocateCommandBuffers = nullptr;
@@ -104,7 +76,7 @@ struct DeviceDispatch {
PFN_vkCmdDispatch CmdDispatch = nullptr;
PFN_vkCmdPipelineBarrier CmdPipelineBarrier = nullptr;
PFN_vkCmdCopyImage CmdCopyImage = nullptr;
PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr; // capture readback
PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr;
PFN_vkCmdBlitImage CmdBlitImage = nullptr;
PFN_vkCmdClearColorImage CmdClearColorImage = nullptr;
PFN_vkCreateFence CreateFence = nullptr;
@@ -120,4 +92,4 @@ static inline void load(PFN& slot, PFN_vkGetDeviceProcAddr gdpa, VkDevice dev, c
slot = reinterpret_cast<PFN>(gdpa(dev, name));
}
} // namespace winfg
}
@@ -1,4 +1,3 @@
// See winfg_engine.h.
#include "winfg_engine.h"
#include "winfg_vkd.h"
#include "log.hpp"
@@ -8,11 +7,6 @@
namespace winfg {
namespace {
// 2x: the guest layer synthesises at 0.35, not 0.5 - biased towards prev to
// cut the double-exposure ghost on HUD/text ("iterative bring-up knob" in
// layer.cpp). Kept for parity, since that is the tuning that was proven.
// 3x/4x: evenly spaced; never exercised by the guest layer, so this is the
// new ground native opens up and the first thing to judge by eye.
float alphaFor(uint32_t k, uint32_t n) {
if (n <= 1) return 0.35f;
return (float)(k + 1) / (float)(n + 1);
@@ -30,7 +24,7 @@ VkImageMemoryBarrier bar(VkImage img, VkImageLayout from, VkImageLayout to,
return b;
}
} // namespace
}
Engine::Engine() = default;
Engine::~Engine() { destroyRing(); fg_.destroy(); }
@@ -65,8 +59,8 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) {
if (unavailable_ || w == 0 || h == 0 || fmt == VK_FORMAT_UNDEFINED) return false;
if (builtW_ == w && builtH_ == h && builtFmt_ == fmt && fg_.valid()) return true;
// onResize rebuilds the pyramids/scratch; our own input ring follows it.
if (!fg_.onResize(VkExtent2D{w, h}, fmt, /*force=*/false)) {
if (!fg_.onResize(VkExtent2D{w, h}, fmt, false)) {
WFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h);
unavailable_ = true;
return false;
@@ -83,21 +77,17 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) {
}
uint32_t Engine::plan(uint32_t capacity) const {
if (!valid() || history_ < 2) return 0; // need prev AND curr
if (!valid() || history_ < 2) return 0;
const uint32_t want = (uint32_t)std::max(cfg_.multiplier - 1, 0);
return std::min({want, capacity, kMaxGenerations});
}
void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h,
uint32_t /*generations*/) {
uint32_t ) {
if (!valid()) return;
// Rotate: the slot we are about to overwrite is the OLD prev; after the
// copy it is the new curr and the other slot is prev.
curr_ = 1 - curr_;
Slot& dst = ring_[curr_];
// source is GENERAL (composite render pass leaves it there); the slot may
// be in any layout from last use - UNDEFINED is legal as "discard".
VkImageMemoryBarrier pre[2] = {
bar(source, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT),
@@ -115,8 +105,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h
dd_.CmdCopyImage(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dst.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
// source back to GENERAL for the real-frame copy that follows; the new
// curr becomes SHADER_READ_ONLY, which is what the chain samples it as.
VkImageMemoryBarrier post[2] = {
bar(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT),
@@ -138,17 +126,11 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou
const Slot& curr = ring_[curr_];
if (!prev.valid || !curr.valid) return;
// The chain writes the output through a STORAGE descriptor in GENERAL.
// The composite ring already keeps its targets in GENERAL; make the
// prior contents' writes visible and take ownership for the compute write.
VkImageMemoryBarrier toWrite = bar(targetImage, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL,
VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT);
dd_.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toWrite);
// gmSlot indexes the C1 global-motion SSBO read back on the CPU next
// frame; kGmSlots (3) >= our frames in flight (2), so rotating per source
// frame keeps a slot from being read while its reduce is still in flight.
fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_);
if (generation + 1 == count) gmSlot_ = (gmSlot_ + 1) % (uint32_t)FrameGen::kGmSlots;
@@ -157,7 +139,6 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou
void Engine::reset() { history_ = 0; frames_ = 0; }
// ---- input ring --------------------------------------------------------------
uint32_t Engine::memType(uint32_t bits, VkMemoryPropertyFlags props) const {
for (uint32_t i = 0; i < memProps_.memoryTypeCount; i++)
@@ -201,4 +182,4 @@ void Engine::destroyRing() {
history_ = 0;
}
} // namespace winfg
}
+4 -37
View File
@@ -1,28 +1,4 @@
#pragma once
// ============================================================================
// winfg_engine - win-fg (Bannerlator's own frame generation) running inside
// the compositor, behind the same contract the LSFG engine uses:
//
// prepare(w, h, format) build/rebuild the chain for this size
// plan(capacity) how many frames to generate this time
// process(cmd, source, w, h, gens) take frame N in (becomes "curr")
// generateInto(cmd, k, n, ...) synthesise generated frame k of n
//
// Why this exists: as a guest-side Vulkan layer, win-fg's generated frames
// had to cross Wine -> DRI3 -> our X server -> the compositor, where the
// per-window latest-wins overwrite dropped them before they were ever
// presented - the HUD counted 2x, the panel got 1x. Here the chain runs in the
// compositor's own command stream and its frames are presented directly, on
// the same multi-present path that is device-proven for LSFG Native.
//
// Unlike LSFG this needs nothing from the user: the ten shaders are ours
// (MIT, see LICENSE.win-fg / NOTICE_FIDELITYFX_OPTICALFLOW.md) and embedded.
//
// The chain (framegen.cpp) is used unmodified. Its inputs are two sampled
// rgba8 views (prev, curr) and its output a GENERAL storage view; the guest
// layer transitions curr to SHADER_READ_ONLY before record() and the output
// UNDEFINED -> GENERAL, so we do exactly the same around the same call.
// ============================================================================
#include <cstdint>
#include <vulkan/vulkan.h>
#include "framegen.hpp"
@@ -32,8 +8,6 @@ struct VkTable;
namespace winfg {
// Hard ceiling on generated frames per source frame (2x..4x -> 1..3), matching
// the LSFG engine and the drawer's buttons.
constexpr uint32_t kMaxGenerations = 3;
class Engine {
@@ -48,22 +22,15 @@ public:
bool valid() const { return fg_.valid() && !unavailable_; }
bool unavailable() const { return unavailable_; }
// model 3/4, perf preset 0..2, flow scale - the same knobs the drawer
// already exposes for win-fg; they map straight onto winfg::Config.
void configure(uint32_t multiplier, int model, int perfPreset, float flowScale);
bool prepare(uint32_t width, uint32_t height, VkFormat format);
// 0 until there is a previous frame to interpolate from.
uint32_t plan(uint32_t capacity) const;
// Copy the composited frame into the input ring as "curr" (the previous
// one becomes "prev"). `source` must be in GENERAL.
void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height,
uint32_t generations);
// Synthesise generated frame `generation` of `count` into the storage view.
// The target must be in GENERAL (the composite ring keeps it there).
void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t count,
VkImage targetImage, VkImageView targetView,
uint32_t width, uint32_t height);
@@ -91,12 +58,12 @@ private:
bool unavailable_ = false;
Slot ring_[2];
int curr_ = 0; // index of "curr" in ring_; prev is 1 - curr_
uint32_t history_ = 0; // frames copied so far (need 2)
uint32_t gmSlot_ = 0; // rotates through FrameGen::kGmSlots
int curr_ = 0;
uint32_t history_ = 0;
uint32_t gmSlot_ = 0;
uint32_t builtW_ = 0, builtH_ = 0;
VkFormat builtFmt_ = VK_FORMAT_UNDEFINED;
uint64_t frames_ = 0;
};
} // namespace winfg
}
@@ -1,4 +1,3 @@
// See winfg_vkd.h.
#include "winfg_vkd.h"
#include "../VulkanRendererContext.h"
#include <android/log.h>
@@ -1,19 +1,4 @@
#pragma once
// ============================================================================
// winfg_vkd - fill win-fg's own dispatch tables from the renderer's VkTable.
//
// win-fg's chain (framegen.cpp / record_impl.inc) was written as a Vulkan
// LAYER and calls through winfg::DeviceDispatch / InstanceDispatch. Inside the
// compositor there is no layer chain to resolve those from, so they are filled
// from the renderer's per-context table instead - the same bridge lsfg_vkd
// provides for the LSFG chain, for the same reason: an adrenotools driver
// shares no global symbols with the system loader.
//
// Only the entry points the chain actually calls are filled (36 device-level
// plus GetPhysicalDeviceMemoryProperties); everything else in the structs
// stays null and is never reached. winfgVkdInit reports a false if any of
// those failed to resolve, so a null is never called mid-frame.
// ============================================================================
#include "vk_dispatch.hpp"
struct VkTable;
bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id);
+1 -29
View File
@@ -1,17 +1,5 @@
#pragma once
// Renderer-neutral direct-scanout implementation (Android SurfaceControl /
// SurfaceFlinger). Pure NDK: dlsym'd ASurfaceControl_* / ASurfaceTransaction_*
// plus AHardwareBuffer_*. It contains ZERO Vulkan/GL/EGL calls and ZERO
// render-thread coupling: the owning renderer drives it through the public
// surface below and decides WHEN the deferred cursor transaction is applied
// (the Vulkan path defers to its render loop; a GL path would apply inline).
//
// Extracted verbatim from VulkanRendererContext / VulkanRendererScanout.cpp
// (P0 of the GL direct-scanout plan). The only intended semantic change vs the
// old code is that the cursor render-thread signalling (needsRender / dirtyCV)
// stays in the owner: setCursorImage()/setCursorPos() return whether an apply
// became pending so the owner can signal its loop exactly as before.
#include <android/hardware_buffer.h>
#include <android/native_window.h>
@@ -27,50 +15,34 @@
class ScanoutContext {
public:
// --- API load / lifecycle ---
bool loadApi();
// Fallback path: creates the child SurfaceControls under the owner's own
// ANativeWindow. Passing a window updates the stored fallback window.
void initFromWindow(ANativeWindow* win = nullptr);
// Primary path: wraps the two sibling game/cursor windows (handed in by the
// owner) as ASurfaceControls. Takes ownership and releases both windows.
void initFromWindows(ANativeWindow* gameWin, ANativeWindow* cursorWin);
void destroy();
// --- per-frame game buffer (applies its transaction INLINE) ---
void setBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1);
// --- cursor: pure state-setters. They mutate state only and DO NOT signal
// any render thread. Each returns true if a deferred apply is now
// pending (i.e. the owner should arrange for applyPendingCursor()).
bool setCursorImage(void* pixels, short w, short h, short stride);
bool setCursorPos(short x, short y, short hotX, short hotY);
// Applies the pending cursor image/position transaction. Safe to call from
// any thread (a SurfaceControl transaction apply is thread-safe). The Vulkan
// owner calls this from its render loop; an inline owner may call it directly.
void applyPendingCursor();
// --- geometry / sizing fed by the owner ---
void setDst(int x, int y, int w, int h);
void setContainerSize(int w, int h) { containerWidth = w; containerHeight = h; }
void setSurfaceSize(int w, int h) { surfaceWidth = w; surfaceHeight = h; }
void setFallbackWindow(ANativeWindow* win) { window = win; }
void setVerboseLog(bool v) { verboseLog = v; }
// --- queries ---
bool isActive() const { return scanoutActive.load(); }
bool isGameFrameDelivered() const {
return gameFrameDelivered.load(std::memory_order_acquire);
}
void* debugGameSC() const { return scanoutGameSC; }
// Public so the owner can expose them through its existing ABI without a
// second source of truth (Vulkan binds references to these).
std::atomic<bool> scanoutActive{false};
std::atomic<bool> gameFrameDelivered{false};
private:
ANativeWindow* window = nullptr; // owner's own window, for the fallback path
ANativeWindow* window = nullptr;
bool verboseLog = true;
int surfaceWidth = 0, surfaceHeight = 0, containerWidth = 0, containerHeight = 0;
-8
View File
@@ -1,11 +1,3 @@
/*
* One function: the release fence the fake-input ring writer needs.
*
* FakeInputWriter publishes events into a shared-memory ring that libfakeinput.so reads from
* another process. Java's memory model says nothing about what a foreign reader sees, so the
* writer calls this between filling the events and advancing the sequence counter: everything
* written before it is visible to the reader before the counter moves.
*/
#include <jni.h>
#include <stdatomic.h>
-1
View File
@@ -1,4 +1,3 @@
// 1116.2.0
#pragma once
const uint32_t cas_code[] = {
0x07230203,0x00010000,0x0008000b,0x000000f3,0x00000000,0x00020011,0x00000001,0x0006000b,
Loaded 100 of 978 files, more files were not shown because too many files have changed in this diff. Show more