This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-13 21:27:42 -04:00
1 parent 4163b31b02
commit 83ec5708ed
852 files changed
+4844 -46144

No files matched your search

-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
-42
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' }}
@@ -211,14 +209,12 @@ 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"
# Cardboard mode's SteamVR driver and Vulkan layer (tools/vr), Vulkan headers from the runner's NDK.
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
@@ -254,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
@@ -273,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
@@ -291,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:
@@ -370,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
@@ -410,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: |
@@ -474,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}}"
@@ -487,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: |
@@ -626,13 +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 #preview-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. Keep this secret name when renaming
# the channel: Discord routes the existing webhook by channel ID, not its display name.
announce:
needs: [build, sign]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
@@ -659,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 }}
@@ -710,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"
@@ -748,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
-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
-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,7 +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/
-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,
-1
View File
@@ -1,4 +1,3 @@
// 1116.5.0
#pragma once
const uint32_t color_code[] = {
0x07230203,0x00010000,0x0008000b,0x00000059,0x00000000,0x00020011,0x00000001,0x0006000b,
-1
View File
@@ -1,4 +1,3 @@
// 1116.2.0
#pragma once
const uint32_t crt_code[] = {
0x07230203,0x00010000,0x0008000b,0x00000063,0x00000000,0x00020011,0x00000001,0x0006000b,
Loaded 100 of 852 files, more files were not shown because too many files have changed in this diff. Show more