mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 21:00:47 +02:00
Compare commits
3
Commits
vr
...
steam-frame
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e409d280ce | ||
|
|
932b9ea7d9 | ||
|
|
7dc4f55642 |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
**/.git
|
||||
/build-frame-arm64/
|
||||
/build/
|
||||
/build-test/
|
||||
/build_*/
|
||||
/frame-ui-local/
|
||||
/rbx_bin/
|
||||
/runtime/
|
||||
/data/
|
||||
/dist/
|
||||
/out/
|
||||
**/__pycache__/
|
||||
*.log
|
||||
*.bundle
|
||||
*.apk
|
||||
*.xapk
|
||||
*.aab
|
||||
*.obb
|
||||
libroblox.so
|
||||
cookies*.txt
|
||||
@@ -2,3 +2,7 @@
|
||||
# snapshot. Local Mocktail sources remain subject to git diff --check.
|
||||
third_party/mcpelauncher-linker/core/** -whitespace
|
||||
packaging/freebsd/*.patch -whitespace
|
||||
|
||||
# Preserve original third-party shader/header whitespace.
|
||||
src/vr/smaa/** -whitespace
|
||||
tools/frame_video_settings/openvr.h -whitespace
|
||||
@@ -1,6 +1,6 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Bug report
|
||||
description: Report a reproducible problem in Nightcap
|
||||
description: Report a reproducible problem on Steam Frame
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
@@ -13,7 +13,6 @@ body:
|
||||
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
@@ -28,8 +27,8 @@ body:
|
||||
id: system
|
||||
attributes:
|
||||
label: System information
|
||||
description: Include your distribution, desktop session, GPU, and graphics driver when relevant.
|
||||
placeholder: "For example: Arch Linux, KDE Plasma 6 on Wayland, AMD RX 6600, Mesa 26.1"
|
||||
description: Include the SteamOS, SteamVR and graphics driver versions.
|
||||
placeholder: "Steam Frame, SteamOS version, SteamVR build, Turnip version"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -38,7 +37,7 @@ body:
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: Tell us how Nightcap was installed or built.
|
||||
placeholder: "For example: AppImage, Flatpak, AUR, or built from source"
|
||||
placeholder: "Source build using the Steam Frame container recipe"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -76,10 +75,8 @@ body:
|
||||
value: |
|
||||
Latest log:
|
||||
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
- Other installs: `~/.local/state/mocktail/logs/latest.log`
|
||||
`~/.local/share/nightcap-vr-arm64/last-launch.log`
|
||||
|
||||
Older logs are in the `sessions` directory next to `latest.log`.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
|
||||
@@ -13,7 +13,6 @@ body:
|
||||
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Briefly describe what this pull request adds, changes, or removes. Link the issue with "Closes #123" or "Fixes #123". -->
|
||||
## Change
|
||||
|
||||
Closes #
|
||||
Describe the behavior changed on Steam Frame and link any relevant issue.
|
||||
|
||||
## Checks
|
||||
|
||||
Describe the build, tests or device checks run for this change.
|
||||
@@ -1,119 +0,0 @@
|
||||
name: Publish AUR packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to publish, such as v0.3.4
|
||||
required: true
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AUR_SSH_PRIVATE_KEY:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: aur-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish nightcap and nightcap-bin
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
environment: release
|
||||
steps:
|
||||
- name: Check out the release
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Render the recipes
|
||||
shell: bash
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ "$TAG" =~ ^v[0-9]+(\.[0-9]+)*$ ]]
|
||||
version="${TAG#v}"
|
||||
archive="nightcap-${version}-1-x86_64.pkg.tar.zst"
|
||||
# nightcap-bin installs the pacman package from the release page, so
|
||||
# its checksum is the one published beside that asset.
|
||||
checksum="$(curl -fsSL \
|
||||
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${archive}.sha256" |
|
||||
awk '{print $1}')"
|
||||
[[ "$checksum" =~ ^[0-9a-f]{64}$ ]]
|
||||
|
||||
sed -i "s/^pkgver=.*/pkgver=${version}/" \
|
||||
packaging/aur/nightcap/PKGBUILD packaging/aur/nightcap-bin/PKGBUILD
|
||||
sed -i "s/^sha256sums=('SKIP')$/sha256sums=('${checksum}')/" \
|
||||
packaging/aur/nightcap-bin/PKGBUILD
|
||||
grep -Fqx "sha256sums=('${checksum}')" packaging/aur/nightcap-bin/PKGBUILD
|
||||
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE/packaging/aur:/aur" \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
useradd --create-home builder
|
||||
for package in nightcap nightcap-bin; do
|
||||
install -d -o builder -g builder "/tmp/${package}"
|
||||
install -o builder -g builder -m 0644 \
|
||||
"/aur/${package}/PKGBUILD" "/tmp/${package}/PKGBUILD"
|
||||
runuser -u builder -- bash -c \
|
||||
"cd /tmp/${package} && makepkg --printsrcinfo" \
|
||||
> "/aur/${package}/.SRCINFO"
|
||||
done
|
||||
'
|
||||
|
||||
for package in nightcap nightcap-bin; do
|
||||
grep -Fqx "pkgver=${version}" "packaging/aur/${package}/PKGBUILD"
|
||||
grep -Fqx $'\tpkgver = '"${version}" "packaging/aur/${package}/.SRCINFO"
|
||||
done
|
||||
echo "VERSION=${version}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Push to the AUR
|
||||
shell: bash
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
if [[ -z "${AUR_SSH_PRIVATE_KEY}" ]]; then
|
||||
echo "::notice::AUR_SSH_PRIVATE_KEY is not set; nothing was pushed to the AUR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ssh_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$ssh_dir"' EXIT
|
||||
install -m 0600 /dev/null "$ssh_dir/key"
|
||||
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$ssh_dir/key"
|
||||
# The AUR lists this key's fingerprint on its front page:
|
||||
# SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
|
||||
printf '%s\n' \
|
||||
'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' \
|
||||
> "$ssh_dir/known_hosts"
|
||||
export GIT_SSH_COMMAND="ssh -i $ssh_dir/key -o IdentitiesOnly=yes -o UserKnownHostsFile=$ssh_dir/known_hosts -o StrictHostKeyChecking=yes"
|
||||
|
||||
for package in nightcap nightcap-bin; do
|
||||
checkout="$ssh_dir/$package"
|
||||
git clone "ssh://aur@aur.archlinux.org/${package}.git" "$checkout"
|
||||
cp "packaging/aur/${package}/PKGBUILD" "packaging/aur/${package}/.SRCINFO" \
|
||||
"$checkout/"
|
||||
git -C "$checkout" add PKGBUILD .SRCINFO
|
||||
if git -C "$checkout" diff --cached --quiet; then
|
||||
echo "::notice::${package} is already at ${VERSION}"
|
||||
continue
|
||||
fi
|
||||
git -C "$checkout" \
|
||||
-c user.name='Nightcap release' \
|
||||
-c user.email='CoderDayton@users.noreply.github.com' \
|
||||
commit --quiet -m "Update to ${VERSION}"
|
||||
# The AUR only accepts the master branch.
|
||||
git -C "$checkout" push origin HEAD:master
|
||||
done
|
||||
@@ -1,56 +0,0 @@
|
||||
name: Check AUR packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: aur-packages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Refresh AUR metadata
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare AUR metadata
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
package_dir="$GITHUB_WORKSPACE/packaging/aur/nightcap-git"
|
||||
version="$(git describe --tags --abbrev=0 --match='v[0-9]*')"
|
||||
version="${version#v}"
|
||||
[[ "$version" =~ ^[0-9]+(\.[0-9]+)*$ ]]
|
||||
pkgver="${version}.r$(git rev-list --count HEAD).g$(git rev-parse --short=7 HEAD)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${pkgver}/" "$package_dir/PKGBUILD"
|
||||
|
||||
docker run --rm \
|
||||
--volume "$package_dir:/pkg" \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
useradd --create-home builder
|
||||
install -d -o builder -g builder /tmp/nightcap-git
|
||||
install -o builder -g builder -m 0644 \
|
||||
/pkg/PKGBUILD /tmp/nightcap-git/PKGBUILD
|
||||
runuser -u builder -- bash -c \
|
||||
"cd /tmp/nightcap-git && makepkg --printsrcinfo" \
|
||||
> /pkg/.SRCINFO
|
||||
'
|
||||
|
||||
grep -Fqx "pkgver=${pkgver}" "$package_dir/PKGBUILD"
|
||||
grep -Fqx $'\tpkgver = '"${pkgver}" "$package_dir/.SRCINFO"
|
||||
|
||||
- name: Upload AUR metadata
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-AUR
|
||||
path: packaging/aur/nightcap-git/.SRCINFO
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -1,136 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
contracts:
|
||||
name: Repository contracts
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Validate shell and JSON files
|
||||
shell: bash
|
||||
run: |
|
||||
find scripts tests -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||
find config packaging/flatpak -type f -name '*.json' -print0 \
|
||||
| xargs -0 -n1 python3 -m json.tool >/dev/null
|
||||
|
||||
- name: Run script unit tests
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m pip install --disable-pip-version-check requests
|
||||
python3 -m unittest discover -s tests -p '*_test.py' -v
|
||||
|
||||
- name: Verify build and packaging contracts
|
||||
shell: bash
|
||||
run: |
|
||||
bash tests/managed_payload_build_contract_test.sh .
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
bash tests/anylinux_dependencies_test.sh .
|
||||
|
||||
native-build:
|
||||
name: Native build (${{ matrix.arch }}, VR ${{ matrix.vr }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-26.04
|
||||
vr: 'ON'
|
||||
- arch: aarch64
|
||||
runner: ubuntu-26.04-arm
|
||||
vr: 'ON'
|
||||
# Packaging recipes build without the OpenXR submodule; keep the
|
||||
# non-VR configuration compiling and passing on its own.
|
||||
- arch: x86_64
|
||||
runner: ubuntu-26.04
|
||||
vr: 'OFF'
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
build-essential binutils cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev libpng-dev zlib1g-dev \
|
||||
xvfb xauth libgl1-mesa-dri jq patchelf
|
||||
|
||||
- name: Build and check native executables
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = "${{ matrix.arch }}"
|
||||
cmake -S . -B build-ci -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON \
|
||||
-DMOCKTAIL_ENABLE_VR=${{ matrix.vr }}
|
||||
cmake --build build-ci --parallel 4
|
||||
./build-ci/mocktail --help
|
||||
./build-ci/mocktail_updater status
|
||||
|
||||
- name: Run C++ and integration tests
|
||||
shell: bash
|
||||
env:
|
||||
SDL_AUDIODRIVER: dummy
|
||||
SDL_VIDEODRIVER: x11
|
||||
LIBGL_ALWAYS_SOFTWARE: '1'
|
||||
run: |
|
||||
xvfb-run -a ctest --test-dir build-ci --output-on-failure \
|
||||
--parallel 4 --timeout 120
|
||||
@@ -1,195 +0,0 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Flatpak
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitmodules'
|
||||
- 'CMakeLists.txt'
|
||||
- 'LICENSE'
|
||||
- 'assets/screenshots/**'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'packaging/icons/**'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.desktop'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.metainfo.xml'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'site/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: flatpak-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
# The signing key lives in the release environment, which only main and
|
||||
# v* tags may use. Builds that do not publish run without it.
|
||||
environment: ${{ (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && 'release' || '' }}
|
||||
env:
|
||||
# Publish on release tags and on manual runs from main. Pull requests
|
||||
# only verify that the manifest still builds.
|
||||
MOCKTAIL_PUBLISH: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak repository and bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
# The manifest builds the local checkout from refs/heads/main.
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/nightcap-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >nightcap-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/nightcap-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=nightcap-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
--disable-rofiles-fuse \
|
||||
--force-clean \
|
||||
--default-branch=stable \
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/nightcap-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Nightcap-x86_64.flatpak \
|
||||
io.github.CoderDayton.nightcap \
|
||||
stable
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Nightcap-x86_64.flatpak nightcap-flatpak.gpg public
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: Nightcap-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
release:
|
||||
name: Attach bundle to GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download Flatpak
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: release
|
||||
|
||||
- name: Upload to release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
(cd release && sha256sum Nightcap-x86_64.flatpak > Nightcap-x86_64.flatpak.sha256)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# The Release workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.flatpak \
|
||||
release/Nightcap-x86_64.flatpak.sha256
|
||||
@@ -1,223 +0,0 @@
|
||||
name: Nightly AppImage
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 05:00 UTC daily. A run whose commit already carries the continuous tag
|
||||
# exits without rebuilding, so a quiet day costs one probe job.
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: nightly-appimage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
probe:
|
||||
name: Check for new commits
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
build: ${{ steps.decide.outputs.build }}
|
||||
steps:
|
||||
- name: Compare HEAD against the published continuous tag
|
||||
id: decide
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
published="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/continuous" \
|
||||
--jq .object.sha 2>/dev/null || true)"
|
||||
if [[ "$GITHUB_EVENT_NAME" == "schedule" && "$published" == "$GITHUB_SHA" ]]; then
|
||||
echo 'build=false' >>"$GITHUB_OUTPUT"
|
||||
echo "continuous already points at $GITHUB_SHA; nothing to build"
|
||||
else
|
||||
echo 'build=true' >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
needs: probe
|
||||
if: needs.probe.outputs.build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
UPINFO: gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|continuous|Nightcap-nightly-x86_64.AppImage.zsync
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
--env UPINFO \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
# Reduced ABI-compatible packages belong only in this builder.
|
||||
# The native updater does not require a JRE or Android SDK.
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-x86_64
|
||||
echo "a4d51752f9861b4870ca529fea04087310b28f8943cc55b99a95b777d4db92ec /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-x86_64-linux
|
||||
echo "0d01a4e2628efc897be9663f0faeed6a171e1851270b4e1ce4c22434a36ecebd /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-x86_64
|
||||
echo "cef962c299f2fa19b2b3cdf2fa1565ee8541796cc89b9a97a591f94041e8b083 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-x86_64
|
||||
echo "50891c38ba359db8271819a6cbf6aaa8068681523f0c4f2b8242007a45edaa28 /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
# Install into the disposable container /usr before deployment.
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Nightcap-nightly-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Nightcap-nightly-x86_64.AppImage
|
||||
test -s dist/Nightcap-nightly-x86_64.AppImage.zsync
|
||||
'
|
||||
|
||||
- name: Smoke-test AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
test "$(dist/Nightcap-nightly-x86_64.AppImage --appimage-updateinformation)" = "$UPINFO"
|
||||
timeout 60s dist/Nightcap-nightly-x86_64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Nightcap-nightly-x86_64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-AppImage
|
||||
path: |
|
||||
dist/Nightcap-nightly-x86_64.AppImage
|
||||
dist/Nightcap-nightly-x86_64.AppImage.zsync
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish continuous release
|
||||
needs: [probe, appimage]
|
||||
# A manual run from another branch still builds and smoke-tests, but the
|
||||
# continuous tag and release only ever follow main.
|
||||
if: needs.probe.outputs.build == 'true' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Nightcap-AppImage
|
||||
path: release
|
||||
|
||||
- name: Checksum the nightly build
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
# The .zsync file names the AppImage it updates, so the build already
|
||||
# carries the published name.
|
||||
test -s release/Nightcap-nightly-x86_64.AppImage
|
||||
test -s release/Nightcap-nightly-x86_64.AppImage.zsync
|
||||
(cd release && sha256sum Nightcap-nightly-x86_64.AppImage \
|
||||
> Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
find release -maxdepth 1 -type f -print
|
||||
|
||||
- name: Update continuous release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git tag --force continuous "$GITHUB_SHA"
|
||||
git push origin refs/tags/continuous --force
|
||||
notes="Automated build of main at ${GITHUB_SHA:0:8}. Untested and unreleased; use a tagged release for normal play."
|
||||
if ! gh release view continuous >/dev/null 2>&1; then
|
||||
gh release create continuous \
|
||||
--prerelease \
|
||||
--title "Nightcap continuous" \
|
||||
--notes "$notes"
|
||||
fi
|
||||
gh release edit continuous --title "Nightcap continuous" --notes "$notes"
|
||||
gh release upload continuous release/* --clobber
|
||||
# Drop assets left by earlier runs under other names.
|
||||
while IFS= read -r asset; do
|
||||
case "$asset" in
|
||||
Nightcap-nightly-x86_64.AppImage|Nightcap-nightly-x86_64.AppImage.zsync|Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
;;
|
||||
*)
|
||||
gh release delete-asset continuous "$asset" --yes
|
||||
;;
|
||||
esac
|
||||
done < <(gh release view continuous --json assets --jq '.assets[].name')
|
||||
@@ -1,350 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check release notes
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: ./scripts/release_notes.sh "$GITHUB_REF_NAME" >/dev/null
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-x86_64
|
||||
echo "a4d51752f9861b4870ca529fea04087310b28f8943cc55b99a95b777d4db92ec /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-x86_64-linux
|
||||
echo "0d01a4e2628efc897be9663f0faeed6a171e1851270b4e1ce4c22434a36ecebd /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-x86_64
|
||||
echo "cef962c299f2fa19b2b3cdf2fa1565ee8541796cc89b9a97a591f94041e8b083 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-x86_64
|
||||
echo "50891c38ba359db8271819a6cbf6aaa8068681523f0c4f2b8242007a45edaa28 /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-x86_64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION="$(./scripts/package_version.sh)" \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
build-essential cmake git ninja-build pkg-config lld fakeroot \
|
||||
dpkg-dev file libsdl3-dev libsdl3-ttf-dev \
|
||||
libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \
|
||||
libyaml-dev libelf-dev libminizip-dev libcapstone-dev \
|
||||
libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev \
|
||||
libvulkan-dev libplacebo-dev zlib1g-dev libpng-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=nightcap \
|
||||
-DMOCKTAIL_PACKAGE_VERSION="$MOCKTAIL_PACKAGE_VERSION" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
-G DEB -B dist/deb
|
||||
dpkg-deb --info dist/deb/*.deb >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-DEB
|
||||
path: dist/deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
rpm:
|
||||
name: RPM (Fedora 44)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION="$(./scripts/package_version.sh)" \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config lld rpm-build \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \
|
||||
webkitgtk6.0-devel utf8proc-devel fontconfig-devel \
|
||||
libglvnd-devel vulkan-headers vulkan-loader-devel \
|
||||
libplacebo-devel zlib-ng-compat-devel libpng-devel
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_PACKAGE_NAME=nightcap \
|
||||
-DMOCKTAIL_PACKAGE_VERSION="$MOCKTAIL_PACKAGE_VERSION" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
-G RPM -B dist/rpm
|
||||
rpm --query --package --info dist/rpm/*.rpm >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-RPM
|
||||
path: dist/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
arch:
|
||||
name: Pacman package (Arch Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build pacman package
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
version="$(./scripts/package_version.sh)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${version}/" packaging/arch/PKGBUILD
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --needed --noconfirm \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libpng \
|
||||
libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
makepkg --dir /workspace/packaging/arch \
|
||||
--noconfirm --cleanbuild --force
|
||||
install -d dist/arch
|
||||
cp packaging/arch/*.pkg.tar.zst dist/arch/
|
||||
pacman -Qp dist/arch/*.pkg.tar.zst >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload pacman package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-Arch
|
||||
path: dist/arch/*.pkg.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: 'Nightcap-*'
|
||||
merge-multiple: true
|
||||
path: release
|
||||
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: release
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
mv release/Mocktail-x86_64.AppImage release/Nightcap-x86_64.AppImage
|
||||
# An unmatched glob would otherwise reach sha256sum and gh as its own
|
||||
# literal text, so every package pattern has to match before upload.
|
||||
shopt -s nullglob
|
||||
assets=(Nightcap-x86_64.AppImage)
|
||||
for glob in '*.deb' '*.rpm' '*.pkg.tar.zst'; do
|
||||
matched=(release/$glob)
|
||||
if [[ ${#matched[@]} -eq 0 ]]; then
|
||||
echo "::error::no ${glob} package among the release artifacts"
|
||||
exit 1
|
||||
fi
|
||||
assets+=("${matched[@]#release/}")
|
||||
done
|
||||
uploads=()
|
||||
for asset in "${assets[@]}"; do
|
||||
uploads+=("release/$asset" "release/$asset.sha256")
|
||||
done
|
||||
(
|
||||
cd release
|
||||
for asset in "${assets[@]}"; do
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
done
|
||||
)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# The Flatpak workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
"${uploads[@]}"
|
||||
|
||||
# nightcap-bin downloads the pacman package from the release page, so this
|
||||
# waits for the upload.
|
||||
aur:
|
||||
name: Publish AUR packages
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: publish
|
||||
uses: ./.github/workflows/aur-publish.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
secrets:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
@@ -0,0 +1,202 @@
|
||||
name: Steam Frame ARM64
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [steam-frame]
|
||||
tags: ['steam-frame-*']
|
||||
pull_request:
|
||||
branches: [steam-frame]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_release:
|
||||
description: Publish a release for an existing steam-frame-* tag
|
||||
type: boolean
|
||||
default: false
|
||||
release_tag:
|
||||
description: Existing tag to build when publishing (leave empty for artifact only)
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: steam-frame-arm64-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
IMAGE: localhost/nightcap-vr-arm64:frame-foveated
|
||||
ASSET: nightcap-steam-frame-arm64.oci.tar.zst
|
||||
|
||||
jobs:
|
||||
source-checks:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release && inputs.release_tag || github.ref }}
|
||||
- name: Check Native settings and package scripts
|
||||
run: |
|
||||
for script in build-frame-nightcap.sh packaging/steam-frame/verify_image.sh; do
|
||||
bash -n "$script"
|
||||
done
|
||||
python3 - <<'PY'
|
||||
import ast
|
||||
from pathlib import Path
|
||||
paths = list(Path('tools/steam_frame').glob('*.py'))
|
||||
paths.append(Path('tools/steam_frame/nightcap-vr-arm64'))
|
||||
for path in paths:
|
||||
ast.parse(path.read_text(), filename=str(path))
|
||||
PY
|
||||
python3 -m unittest discover -s tests -p 'steam_frame_*_test.py'
|
||||
node tests/steamvr_video_controls_test.js
|
||||
|
||||
build:
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: source-checks
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 90
|
||||
outputs:
|
||||
release_tag: ${{ steps.release-target.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release && inputs.release_tag || github.ref }}
|
||||
- name: Validate release target
|
||||
id: release-target
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
REF_TYPE: ${{ github.ref_type }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
PUBLISH: ${{ inputs.publish_release }}
|
||||
REQUESTED_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
tag=''
|
||||
if [[ "$EVENT" == push && "$REF_TYPE" == tag ]]; then
|
||||
tag="$REF_NAME"
|
||||
elif [[ "$EVENT" == workflow_dispatch && "$PUBLISH" == true ]]; then
|
||||
tag="$REQUESTED_TAG"
|
||||
fi
|
||||
if [[ -n "$tag" || "$PUBLISH" == true ]]; then
|
||||
[[ "$tag" =~ ^steam-frame-[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
|
||||
[[ "$(git rev-parse "refs/tags/$tag^{commit}")" == "$(git rev-parse HEAD)" ]]
|
||||
fi
|
||||
printf 'tag=%s\n' "$tag" >> "$GITHUB_OUTPUT"
|
||||
- name: Install container tools
|
||||
run: |
|
||||
test "$(uname -m)" = aarch64
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y podman zstd
|
||||
- name: Build Native ARM64 image
|
||||
run: |
|
||||
sudo podman build --arch arm64 --jobs 1 \
|
||||
--label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \
|
||||
--label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \
|
||||
--label org.opencontainers.image.title='Nightcap Steam Frame ARM64' \
|
||||
-f packaging/steam-frame/Containerfile -t "$IMAGE" .
|
||||
- name: Run mirror and runtime configuration regressions
|
||||
run: |
|
||||
sudo podman run --rm --entrypoint /bin/bash "$IMAGE" -lc '
|
||||
set -euo pipefail
|
||||
cmake -S /work -B /work/build-frame-arm64 -DBUILD_TESTING=ON
|
||||
cmake --build /work/build-frame-arm64 -j3 \
|
||||
--target desktop_mirror_test runtime_config_test runtime_config_file_test
|
||||
/work/build-frame-arm64/tests/desktop_mirror_test
|
||||
/work/build-frame-arm64/tests/runtime_config_test
|
||||
/work/build-frame-arm64/tests/runtime_config_file_test
|
||||
'
|
||||
- name: Check payload-free image
|
||||
run: |
|
||||
mkdir -p dist
|
||||
sudo bash packaging/steam-frame/verify_image.sh "$IMAGE" > dist/image-check.json
|
||||
cat dist/image-check.json
|
||||
- name: Export image and checksums
|
||||
run: |
|
||||
sudo podman save --format oci-archive "$IMAGE" | zstd -T2 -6 -o "dist/$ASSET"
|
||||
test "$(stat -c %s "dist/$ASSET")" -lt 2147483648
|
||||
python3 - <<'PY'
|
||||
import json, os, pathlib, subprocess, tarfile
|
||||
archive = pathlib.Path('dist') / os.environ['ASSET']
|
||||
process = subprocess.Popen(['zstd', '-dc', str(archive)], stdout=subprocess.PIPE)
|
||||
index = None
|
||||
with tarfile.open(fileobj=process.stdout, mode='r|') as stream:
|
||||
for member in stream:
|
||||
if member.name == 'index.json':
|
||||
index = json.load(stream.extractfile(member))
|
||||
while process.stdout.read(1024 * 1024):
|
||||
pass
|
||||
if process.wait() != 0:
|
||||
raise SystemExit('Compressed OCI archive integrity check failed')
|
||||
manifests = index.get('manifests', []) if index else []
|
||||
if (len(manifests) != 1 or manifests[0].get('annotations', {}).get(
|
||||
'org.opencontainers.image.ref.name') != os.environ['IMAGE']):
|
||||
raise SystemExit('OCI archive must restore the normal launcher image tag')
|
||||
print('Compressed OCI archive and launcher tag verified')
|
||||
PY
|
||||
cd dist
|
||||
sha256sum "$ASSET" > "$ASSET.sha256"
|
||||
- name: Write build metadata
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.release-target.outputs.tag }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, pathlib, subprocess
|
||||
commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
|
||||
image_id = subprocess.check_output(['sudo', 'podman', 'image', 'inspect', '--format', '{{.Id}}', os.environ['IMAGE']], text=True).strip()
|
||||
metadata = {
|
||||
'source': 'https://github.com/' + os.environ['GITHUB_REPOSITORY'],
|
||||
'commit': commit, 'release_tag': os.environ['RELEASE_TAG'] or None,
|
||||
'platform': 'linux/arm64', 'image_tag': os.environ['IMAGE'], 'image_id': image_id,
|
||||
'archive': os.environ['ASSET'], 'upscaling': 'Native', 'fsr3_compiled': False,
|
||||
'default_eye_size': [2160, 2160], 'default_refresh_hz': 72,
|
||||
'default_antialiasing': 'SMAA High',
|
||||
'checks': ['Native single-sample policy', 'refresh-rate policy', 'desktop mirror policy', 'video controls',
|
||||
'mirror command recording and runtime configuration',
|
||||
'AArch64 ELF and dynamic dependencies', 'payload and license packaging',
|
||||
'compressed OCI integrity and launcher tag'],
|
||||
'runtime_testing': 'CI packaging checks do not start SteamVR or Roblox',
|
||||
'licenses': 'Source notices and third-party notices are included under /work in the image',
|
||||
}
|
||||
pathlib.Path('dist/build-info.json').write_text(json.dumps(metadata, indent=2) + '\n')
|
||||
PY
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: nightcap-steam-frame-arm64
|
||||
path: dist/
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
retention-days: 14
|
||||
|
||||
release:
|
||||
if: needs.build.outputs.release_tag != ''
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: nightcap-steam-frame-arm64
|
||||
path: dist
|
||||
- name: Publish checked ARM64 package
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RELEASE_TAG: ${{ needs.build.outputs.release_tag }}
|
||||
run: |
|
||||
cd dist
|
||||
sha256sum --check "$ASSET.sha256"
|
||||
printf '%s\n' \
|
||||
'Native ARM64 Steam Frame image and packaging-check metadata.' \
|
||||
'Roblox, SteamVR and host graphics drivers are downloaded or provided separately.' \
|
||||
'Verify the SHA256, then import with: zstd -dc nightcap-steam-frame-arm64.oci.tar.zst | podman load' \
|
||||
'See docs/INSTALL.md in this tag for launcher and SteamVR setup.' > release-notes.md
|
||||
gh release create "$RELEASE_TAG" --verify-tag \
|
||||
--title "Steam Frame ARM64 $RELEASE_TAG" --notes-file release-notes.md \
|
||||
"$ASSET" "$ASSET.sha256" image-check.json build-info.json
|
||||
+17
@@ -180,3 +180,20 @@ lsan.log*
|
||||
|
||||
# Working plans, not part of the shipped documentation
|
||||
/docs/plans/
|
||||
|
||||
# Steam Frame local builds and generated SteamVR assets
|
||||
/build-frame-arm64/
|
||||
/frame-ui-local/
|
||||
/tools/steam_frame/steamvr-*.original.*
|
||||
/tools/steam_frame/steamvr-*.sharpening.*
|
||||
/tools/steam_frame/steamvr-sharpening-ui-patch.json
|
||||
/steam-shortcut.json
|
||||
|
||||
# Local Frame payloads, images and source-transfer archives
|
||||
/data/
|
||||
/host-drivers/
|
||||
*.bundle
|
||||
*.oci
|
||||
*.img
|
||||
/current.json
|
||||
/previous_good.json
|
||||
@@ -7,6 +7,3 @@
|
||||
[submodule "third_party/OpenXR-SDK"]
|
||||
path = third_party/OpenXR-SDK
|
||||
url = https://github.com/KhronosGroup/OpenXR-SDK.git
|
||||
[submodule "third_party/smaa"]
|
||||
path = third_party/smaa
|
||||
url = https://github.com/iryoku/smaa.git
|
||||
-322
@@ -1,322 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Nightcap. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
Release notes on GitHub are taken from the matching section here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- An asset overlay folder, `~/.local/share/mocktail/asset_overlay`. A file there replaces the file Roblox
|
||||
ships at the same path, such as a cursor, a font or a sky texture, and it stays in place when Roblox
|
||||
updates. The layout matches Sober's `asset_overlay` folder. `MOCKTAIL_ASSET_OVERLAY_DIR` moves it. Thanks to
|
||||
@dim-ghub.
|
||||
- The asset overlay folder now also replaces files Roblox downloads and no longer ships, such as the shift
|
||||
lock and text cursors. Put the file under `content/`, at the path Roblox uses. Thanks to @dim-ghub for the
|
||||
idea.
|
||||
- `MOCKTAIL_ASSET_TRACE=1` now also prints the files Roblox asks for and does not get, as `[asset] miss`.
|
||||
- Arch Linux: `nightcap-git` is on the AUR and builds the current `main` branch. Thanks to @dim-ghub, who
|
||||
maintains it.
|
||||
- `vr.mode` in `config.yaml`: `off` starts on the desktop, `on` starts in VR, and `auto` starts in VR only
|
||||
when SteamVR is already running with a connected headset. `auto` never starts SteamVR. `vr.enabled` still
|
||||
works.
|
||||
- Anti-aliasing in VR. Roblox draws the VR eyes without it, so edges were jagged in the headset. Each eye is
|
||||
now filtered on its way to the headset. `vr.anti_aliasing` picks `fxaa` (the default), `smaa`, which finds
|
||||
edges more precisely and keeps text sharper for a little more GPU time, or `off`.
|
||||
- The log now names each Roblox Java call that Nightcap has no handler for, once per call, as
|
||||
`[JNI] no handler for <class>.<method><signature>`. This shows what a new Roblox version needs.
|
||||
- `--profile` traces now show how much GPU memory decoded ETC2 textures take, as the `emulated texture (MB)`
|
||||
and `compressed texture (MB)` counters. `scripts/summarize_profile_trace.py` prints the peak of each.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Joining a game from a website link while already in a game no longer freezes Nightcap. It leaves the
|
||||
current game and joins the new one, and leaving that game returns to the home screen.
|
||||
|
||||
### Changed
|
||||
|
||||
- The home screen and avatar editor have fewer long frames. PC profiles compile the app's menu scripts to
|
||||
native code and turn this off while you are in a game, so games run as before. Set
|
||||
`FFlagDebugLuauCodegenAll` in `fflags.json` to choose it yourself.
|
||||
- Nightcap answers Roblox's Java calls with less work per call, and the audio thread no longer allocates
|
||||
memory for each block of sound it plays.
|
||||
- VR on Vulkan no longer makes the CPU wait for the GPU each frame. The eye copies, the desktop mirror and
|
||||
the `MOCKTAIL_VR_MIRROR_SHM` readback are ordered on the GPU, so Roblox prepares the next frame while the
|
||||
last one renders. On a simulated 120 Hz headset this removed 2 to 5 ms of waiting per frame and steadied
|
||||
the frame rate. The shared-memory mirror now shows each frame one frame later.
|
||||
- `MOCKTAIL_VR_PERF` also reports `desktop_mirror`, `present_prepare` and `present_interval`.
|
||||
|
||||
## [0.3.4] - 2026-10-11
|
||||
|
||||
### Added
|
||||
|
||||
- The AppImage build now makes a `.zsync` file, so tools like `appimageupdatetool` can update the AppImage.
|
||||
- The desktop side panel menu now shows by default on PC profiles. Set `FIntFlyoutTestVariantRollout12` in
|
||||
`fflags.json` to change it.
|
||||
|
||||
### Changed
|
||||
|
||||
- The Flatpak build asks for the X11 socket only when there is no Wayland session, as before 0.3.3.
|
||||
- Nightcap now uses Wayland on a Wayland session with NVIDIA drivers too, however it is started. It used to
|
||||
fall back to X11 there when started from a terminal.
|
||||
- Building Nightcap no longer needs libexecinfo.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Roblox 2.742 being rejected as an update, which left the game on the older version. Nightcap now
|
||||
accepts Roblox versions that change the order of their start-up steps.
|
||||
- Fixed Nightcap reporting Mocktail's version number, 1.0.4, in session logs and support bundles. It now
|
||||
reports its own version.
|
||||
- Fixed the old Roblox home screen showing instead of the new one with the Me tab and the "For you" and
|
||||
"Charts" tabs. Nightcap now runs the Roblox start-up step that loads the new interface for your account.
|
||||
- Fixed anti-aliasing being forced off at every graphics quality level. Roblox now picks it, and a
|
||||
`FIntDebugForceMSAASamples` value in `fflags.json` still applies. Thanks to @qutad.
|
||||
- Fixed the voice chat microphone only working on Roblox 2.738. It no longer depends on one Roblox version.
|
||||
- Fixed other players not hearing you in voice chat after a Roblox update. The updater now rebuilds the
|
||||
audio device data when the previous version's profile has none.
|
||||
- Fixed frame hitches while textures load on graphics cards that need ETC2 textures decoded on the CPU.
|
||||
- Fixed problems on Chimera Linux and other musl systems, including error messages that differed from the
|
||||
ones Roblox expects.
|
||||
- Fixed the nightly AppImage build failing when a bundled GTK 4 was older than the one libadwaita needs.
|
||||
- Fixed a random crash on Wayland with NVIDIA drivers, where the game window closed after you let go of the
|
||||
camera or left mouse lock. The log showed `wp_linux_drm_syncobj_surface_v1` error 3.
|
||||
- Fixed some menu actions being ignored. The Chat button on a profile now opens the Party tab with that
|
||||
player, and clicking a search suggestion now runs the search instead of closing the search menu.
|
||||
- Fixed possible crashes when the game calls an Android function Nightcap did not provide, or releases the
|
||||
same reference twice.
|
||||
- Fixed a possible hang on close when a web view process stayed open. Nightcap now stops waiting for the log
|
||||
writer after 2 seconds.
|
||||
- Fixed a possible missing picture on graphics drivers that reject the low-latency present mode. Nightcap now
|
||||
retries with the game's own mode.
|
||||
- Fixed a possible Wayland crash when the window first appears on the OpenGL graphics path.
|
||||
- Fixed a missing picture on machines where the graphics card runs a different driver than the one Nightcap
|
||||
picked, such as an NVIDIA card on nouveau or a laptop with an Intel chip and an older second card. Nightcap
|
||||
now picks the Vulkan driver that matches the running kernel driver, and says so when no card can run Vulkan.
|
||||
|
||||
## [0.3.3] - 2026-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- Joining a friend from a Roblox "follow user" link now works. Nightcap looks up which game they are in and
|
||||
joins it.
|
||||
- Nix users get a desktop entry for the app.
|
||||
|
||||
### Changed
|
||||
|
||||
- Roblox textures now decode about 3 times faster, which shortens the stalls when a place loads and when new
|
||||
parts of the map stream in. Thanks to @glook9001.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `make update-roblox` no longer deletes your previous Roblox install when an import fails partway through. If
|
||||
a step fails, the old files are put back, or kept in a folder whose path is printed. Thanks to @kimlherme.
|
||||
- A failed Roblox update now restores your previous version's records instead of leaving them half switched. A
|
||||
damaged version file is rejected instead of being half read, and a temporary failure to start the startup
|
||||
check no longer marks a working Roblox build as broken for good. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when Roblox clears its audio queue while sound is still playing, and a memory leak in
|
||||
lists of objects passed to Roblox's Java code. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when the text overlay shuts down, a freeze when Roblox releases the mouse lock while
|
||||
another release is already in progress, and network lookups that misread Roblox's options. Thanks to
|
||||
@kimlherme.
|
||||
- Fixed the sky sometimes rendering with the wrong textures in the Vulkan renderer.
|
||||
- Clicking while a Roblox text box has focus no longer closes it. Only Roblox decides when a click ends
|
||||
typing.
|
||||
- Going to another page in the web view, such as Friends from a profile, no longer closes the web view. It
|
||||
crashed on every page change on Wayland systems whose GL setup GTK could not use.
|
||||
- Fixed a crash when Roblox's saved dark or light theme setting was missing or damaged.
|
||||
|
||||
## [0.3.2] - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Performance recordings made with `--profile` now show how busy the CPU was during each frame, which makes it
|
||||
easier to tell what caused a stutter.
|
||||
- Experimental support for ARM64 (`aarch64`) Linux, running Roblox's Android `arm64-v8a` client.
|
||||
|
||||
### Fixed
|
||||
|
||||
- When Roblox asks for a Google Play device check during login, Nightcap now answers right away that it isn't
|
||||
supported. Before, the "Verifying you're not a bot" screen waited about a minute for an answer and then sent
|
||||
you back to the login screen.
|
||||
- Solving a login challenge now closes its window. Before, Roblox's "challenge completed" signal was ignored
|
||||
and the window stayed open. Thanks to @glook9001.
|
||||
- Login no longer stalls after you pass a verification check. Before, Roblox only saw the page's "challenge
|
||||
completed" signal after a one-minute timeout, so it gave up and the login failed. On a Roblox build Nightcap
|
||||
can't handle this way, the log says "WebViewProtocol getter not recognized" and web pages behave as before.
|
||||
- Login check windows and other web pages get 5 seconds to start instead of 3, so a slow start no longer
|
||||
closes them. The log says how long each one took to open.
|
||||
- Less stutter while joining a game and while textures load in. In Blade Ball, loading hitches dropped by
|
||||
about half, and the worst one went from 0.8 seconds to 0.55 seconds.
|
||||
- Clicking Play on a private server now joins right away. Before, nothing happened until you clicked a sidebar
|
||||
tab like Home.
|
||||
- Middle click now reaches Roblox as the middle mouse button.
|
||||
- Scrolling follows your system's natural scrolling setting.
|
||||
- Fixed a crash on some x86_64 hosts when Roblox asked about a thread other than the one making the call.
|
||||
- Roblox updates no longer mix up the ARM64 and x86_64 compatibility profiles for the same version.
|
||||
- A partial or out-of-range user id in `MOCKTAIL_ROBLOX_USER_ID` or `MOCKTAIL_LOCAL_STORAGE_CURRENT_USER_ID`
|
||||
is now ignored instead of being partly read.
|
||||
|
||||
### Changed
|
||||
|
||||
- `make flatpak` and `scripts/build_flatpak.sh` now build with 2 jobs by default instead of 4, so a local
|
||||
Flatpak build is less likely to run out of memory. Set `FLATPAK_JOBS` or `MOCKTAIL_FLATPAK_JOBS` to use
|
||||
more. Thanks to @glook9001.
|
||||
|
||||
## [0.3.1] - 2026-09-20
|
||||
|
||||
### Added
|
||||
|
||||
- Each release carries a DEB, an RPM, and a pacman package beside the AppImage, with a `.sha256` for every
|
||||
file. They install `/usr/bin/mocktail` and conflict with each other and with upstream's `mocktail` package.
|
||||
Nightcap is in no distribution repository or the AUR; the recipes under `packaging/aur/nightcap{,-bin,-git}`
|
||||
build from this repository locally.
|
||||
- The AppStream metadata lists the Nightcap releases, so software centres show the release history instead of
|
||||
upstream's.
|
||||
|
||||
### Fixed
|
||||
|
||||
- ETC2 texture decoding no longer blocks the thread that submits the frame. Uploads decode on a background
|
||||
dispatcher and the submit carries a timeline semaphore wait, so the GPU waits for the texture instead of the
|
||||
render thread. Reading the application's compressed bytes moves to that dispatcher too whenever they are
|
||||
already mapped. In Blade Ball the worst `vkQueueSubmit` fell from 140ms to 1ms, its p99 from 4.9ms to
|
||||
0.05ms, and the total time spent in submits from 2534ms to 309ms. Frame interval is unchanged: the remaining
|
||||
tail is the engine's own main-thread step. A device without the `timelineSemaphore` feature decodes inline
|
||||
as before, as does a submit whose `pNext` already sizes arrays by its semaphore counts.
|
||||
- The mouse pointer jumped when a right-click camera drag ended, and drifted to the viewport edge during one.
|
||||
The input router inferred pointer capture from SDL reporting zero absolute coordinates, which holds on X11
|
||||
but not on Wayland, where the reported position accumulates the relative deltas and leaves the window.
|
||||
Motion, button, and wheel events now carry the window's relative mouse mode. The right-click camera fallback
|
||||
also only captures inside an experience, so the cursor stays free over the app surface.
|
||||
- Shutting the window down while an experience was starting or leaving could free the pointer capture owner
|
||||
underneath the thread using it. Callers off the SDL thread now hold a reference for the length of their
|
||||
call.
|
||||
- The TextBox caret did not move when a trailing space was typed in a single-line field. The final cluster
|
||||
rect SDL_ttf reports stops at the last inked glyph, so the laid-out width is used there instead. Wrapped
|
||||
layouts keep the clamp, because their width is the widest line rather than the last.
|
||||
- Every TextBox keystroke re-ran `TTF_Init`, a fontconfig sort, six `TTF_OpenFont` calls, and `TTF_Quit` on
|
||||
the render thread. The overlay holds its opened fonts and reopens them only when the Roblox font or the
|
||||
point size changes, keeping the current set when a reopen fails. Measured over 100 rasters, 1.59ms to 0.12ms
|
||||
per keystroke.
|
||||
- Staged ETC2 copies now carry an explicit host-write barrier. A queue submission only makes host writes from
|
||||
before it visible to the device, and an asynchronous decode writes after the submit, so without the barrier
|
||||
those texels were not guaranteed to reach the copy that reads them.
|
||||
|
||||
### Changed
|
||||
|
||||
- `docs/PERFORMANCE.md` documents the GameMode settings to run with. A `desiredgov=powersave` in
|
||||
`~/.config/gamemode.ini` overrides the packaged `performance` default and clocks the CPU down for the whole
|
||||
session while still reporting an active performance request.
|
||||
|
||||
## [0.3.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
|
||||
- Custom Discord presence. `integrations.discord_rpc.text.title` replaces the name in "Playing Nightcap", and
|
||||
`integrations.discord_rpc.images` sets the large and small icons and their hover text. Text and image fields
|
||||
accept `{place_name}`, `{place_icon}`, and `{creator_name}`. A field that expands an empty placeholder is
|
||||
left out, and `text.state: ""` hides the state line.
|
||||
- The default Discord card now matches Sober: the title is "Roblox", the details line is "Playing
|
||||
<experience>", the state is "by <creator>", and the Roblox badge sits inside the experience icon. The
|
||||
creator comes from the same Roblox games request that already fetches the experience name. With
|
||||
`show_place_name: false` the creator is hidden too, so the card shows only the title and timer.
|
||||
- `--profile <file>` writes a Chrome-format trace of the Vulkan adapter's work: presents, GPU waits, submits,
|
||||
ETC2 texture decode, and pipeline and shader creation. Open it in ui.perfetto.dev. `docs/BENCHMARKING.md`
|
||||
describes the benchmark runs, and `scripts/summarize_profile_trace.py` turns traces into a before and after
|
||||
table.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Texture uploads stalled the frame they were submitted on. ETC2 batches decoded on worker threads but were
|
||||
then resampled one upload at a time on the submitting thread, which at the default 4x upscale is sixteen
|
||||
times the decoded texels. Resampling now runs on the same worker pool, in two passes so mip level 0 still
|
||||
resolves before the mips built from it. In a 70k-part place the worst `vkQueueSubmit` fell from 167ms to
|
||||
92ms.
|
||||
- Most ETC2 batches decoded on a single thread. Workers were created and joined per batch, so the parallel
|
||||
floor had to cover that cost and sat at a 1024x1024 image; batches under it ran on the caller, which was 54%
|
||||
of all decode time in a 70k-part place. Decode workers are now a persistent pool, and the floor drops to a
|
||||
128x128 image. Mean decode time in that place fell from 8.2ms to 4.7ms, and 11.8ms before both texture
|
||||
fixes.
|
||||
- On CPUs with a wide core-speed spread, Feral GameMode pinned the process to the few fastest cores,
|
||||
concentrating Roblox's worker pool onto them instead of spreading it. With `performance.gamemode: auto`, a
|
||||
request that narrows the CPU affinity is now released again, which releases the pin. Set `pin_cores=no` in
|
||||
`~/.config/gamemode.ini` to keep GameMode without the pinning, or `performance.gamemode: on` to accept it.
|
||||
See `docs/PERFORMANCE.md`.
|
||||
- With `show_place_name: false`, Discord no longer shows the experience thumbnail or its name on hover.
|
||||
- The host main thread no longer spins on the engine's message pump, which cost about half a core and 146,000
|
||||
empty calls a second. It now rests 100 µs between polls: sleeping when the governor is `performance`,
|
||||
parking the core with TPAUSE on CPUs that have it, spinning otherwise. With GameMode setting the governor,
|
||||
process CPU in a 60k-part place fell from 216% to 107% at the same 120 fps. `MOCKTAIL_ENGINE_PUMP_REST`
|
||||
forces a mode; see `docs/PERFORMANCE.md`.
|
||||
- ETC2 uploads with a `bufferRowLength` or `bufferImageHeight` larger than the image read the padded rows as
|
||||
packed data and decoded garbage.
|
||||
- ETC2 resampling created and joined worker threads on every batch. It now runs on the persistent decode pool.
|
||||
- A failed trace write left the profile writer thread waking every 250 ms for the rest of the session. It now
|
||||
exits.
|
||||
- The `--profile` trace gains a `pump` slice for the engine's main-thread step.
|
||||
|
||||
## [0.2.0] - 2026-09-16
|
||||
|
||||
### Added
|
||||
|
||||
- FAQ covering Discord RPC, log locations, and VR, adapted from upstream.
|
||||
- `input.raw_mouse` in `config.yaml` sends mouse look deltas in the host's own units. Turn it on when
|
||||
first-person aiming feels too slow or too fast under fractional display scaling. Off by default.
|
||||
- Nightly AppImage builds, published as the `continuous` prerelease. They are untested; use a tagged release
|
||||
for normal play.
|
||||
|
||||
### Changed
|
||||
|
||||
- Values in `fflags.json` now win over the performance preset instead of stopping the launch. Startup prints
|
||||
each key it kept.
|
||||
- Discord Rich Presence uses Nightcap's own Discord application, so it shows as "Playing Nightcap". Terms and
|
||||
privacy pages for it live on the project site.
|
||||
- Discord Rich Presence is on by default. Set `integrations.discord_rpc.enabled: false` to turn it off.
|
||||
- The AppImage build verifies its reduced dependency packages against digests taken at build time, rather than
|
||||
a digest list kept in the repository.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mouse look keeps responding after a long turn. The tracked pointer position could drift past the edge of the
|
||||
view, which swallowed every reversal until the mouse travelled all the way back.
|
||||
- Nightcap no longer refuses to start with `cannot lock appStorage`. It created its own credential directory
|
||||
using the process umask, then rejected that directory for being group writable. Systems with a permissive
|
||||
umask, such as `002`, failed on first run.
|
||||
|
||||
## [0.1.0] - 2026-09-15
|
||||
|
||||
First Nightcap release, forked from komaruworld/mocktail.
|
||||
|
||||
### Added
|
||||
|
||||
- ETC2 and EAC texture emulation on Vulkan, so games that use compressed mobile textures render correctly on
|
||||
desktop GPUs. Decoding runs on worker threads to avoid stutter.
|
||||
- PNG texture overrides in `~/.config/mocktail/textures`, keyed by upload hash, with a dump mode to find the
|
||||
texture you want to replace.
|
||||
- Small textures are upscaled 4x by default. Set `MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn it off.
|
||||
- AppImage and signed Flatpak builds on every release tag. The Flatpak repo is published at
|
||||
https://coderdayton.github.io/nightcap/.
|
||||
- Roadmap in `docs/ROADMAP.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Frames are presented as soon as they are ready instead of waiting in a queue, which lowers input latency.
|
||||
- Roblox now sees the real host RAM and display size instead of a low-end phone profile.
|
||||
- App ID is `io.github.CoderDayton.nightcap`, so Nightcap installs next to upstream Mocktail. The binary and
|
||||
config paths keep the `mocktail` name.
|
||||
- New name, icon, and colors.
|
||||
- The desktop entry launches on Wayland and points at the installed binary.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Servers no longer kick with error 319 shortly after joining.
|
||||
- The FMOD output-device bridge is optional, so the updater no longer fails without it.
|
||||
- The AppImage build no longer fails on stale checksums for the slimmed Arch packages.
|
||||
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.4...HEAD
|
||||
[0.3.4]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.4
|
||||
[0.3.3]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.2
|
||||
[0.3.1]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.1
|
||||
[0.3.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.0
|
||||
[0.2.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.2.0
|
||||
[0.1.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.1.0
|
||||
@@ -820,3 +820,17 @@ add_dependencies(mocktail mocktail_webview_helper)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
# Native SteamVR identity/settings helper; a background client never takes scene focus.
|
||||
add_executable(nightcap-vr-settings tools/frame_video_settings/nightcap-vr-settings.cc)
|
||||
target_include_directories(nightcap-vr-settings PRIVATE tools/frame_video_settings)
|
||||
target_link_libraries(nightcap-vr-settings PRIVATE ${CMAKE_DL_LIBS})
|
||||
install(TARGETS nightcap-vr-settings RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
if(MOCKTAIL_ENABLE_VR)
|
||||
add_executable(nightcap-vr-performance tools/frame_video_settings/performance_overlay.cc)
|
||||
target_include_directories(nightcap-vr-performance PRIVATE include tools/frame_video_settings)
|
||||
target_link_libraries(nightcap-vr-performance PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf
|
||||
PkgConfig::FONTCONFIG ${CMAKE_DL_LIBS})
|
||||
install(TARGETS nightcap-vr-performance RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
endif()
|
||||
@@ -1,79 +0,0 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
# FAQ
|
||||
|
||||
## How do I turn Discord RPC off?
|
||||
|
||||
RPC is on by default. It never signs in to Discord and never reads an
|
||||
account token. To turn it off, launch Nightcap once to create the config,
|
||||
then close it. Open `config.yaml`:
|
||||
|
||||
- Native / AppImage: `~/.config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/config/mocktail/config.yaml`
|
||||
|
||||
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
|
||||
|
||||
Set `enabled` to `false` under `integrations.discord_rpc`. If the block is
|
||||
missing, add it under the existing `integrations` section:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
discord_rpc:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Save the file and restart Nightcap. Set `enabled` back to `true` to turn
|
||||
RPC on again.
|
||||
|
||||
## Can Discord count Nightcap as Roblox play time or quests?
|
||||
|
||||
No. Discord only credits play time and quests to games its own scanner
|
||||
detects, and Rich Presence never counts. On Linux the scanner only matches
|
||||
games with a Linux entry in Discord's list. Roblox has none, so no process
|
||||
name Nightcap could use would be detected. Only Roblox or Discord can change
|
||||
that.
|
||||
|
||||
## Where can I find logs for a bug report?
|
||||
|
||||
Nightcap saves logs automatically. Reproduce the issue, close Nightcap, and
|
||||
attach `latest.log`:
|
||||
|
||||
- Native / AppImage: `~/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
|
||||
|
||||
## Can I run multiple Roblox instances?
|
||||
|
||||
No. Nightcap won't add multi-instance launching. It would mean working around
|
||||
Roblox's restrictions on running several clients. The [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use)
|
||||
prohibit bypassing technical protections, and violating the terms can get your
|
||||
account suspended.
|
||||
|
||||
Bloxstrap brought the option back in [v2.9.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.9.0)
|
||||
with a "use at your own risk" warning, then removed it in
|
||||
[v2.10.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.10.0)
|
||||
after Roblox added measures against running multiple clients.
|
||||
|
||||
## Can I play in VR?
|
||||
|
||||
Yes, as an experiment. VR lives on Nightcap's `vr` branch and is not in the
|
||||
releases yet. It works on Roblox builds 2998, 3092 and 3222. Install the
|
||||
[build dependencies](README.md#building), then switch to that branch and
|
||||
build:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git switch vr
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
```
|
||||
|
||||
Start WiVRn or SteamVR/ALVR, connect your headset, then run:
|
||||
|
||||
```bash
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
The [README](README.md#experimental-vr) and [docs/VR.md](docs/VR.md) have the
|
||||
details, including how to try it without a headset.
|
||||
@@ -1,313 +1,62 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/io.github.CoderDayton.nightcap.svg" alt="Nightcap logo" width="128">
|
||||
<h1 style="margin-top: 5px;">
|
||||
Nightcap
|
||||
<br>
|
||||
<a href="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml"><img src="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<a href="https://github.com/CoderDayton/nightcap/stargazers"><img src="https://img.shields.io/github/stars/CoderDayton/nightcap?style=flat&logo=github" alt="Stars"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License"></a>
|
||||
<a href="https://discord.gg/DbAUG9hBFU"><img alt="Discord" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Fv10%2Finvites%2FDbAUG9hBFU%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&label=Discord&logo=discord&logoColor=white&color=5865F2&suffix=%20members"></a>
|
||||
</h1>
|
||||
<img src="packaging/io.github.CoderDayton.nightcap.svg" alt="Nightcap logo" width="128">
|
||||
<h1>Nightcap Roblox VR for Steam Frame</h1>
|
||||
</div>
|
||||
|
||||
Nightcap is Roblox on Linux, tuned for desktop play. It is a Mocktail fork
|
||||
that keeps up with Roblox updates, with extra bug fixes, custom assets and
|
||||
more. It runs the Android `x86_64` Roblox client on Linux, including a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
|
||||
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
|
||||
the Linux side.
|
||||
Native ARM64 Roblox VR for Steam Frame, with eye-tracked foveated rendering,
|
||||
controller input and SteamVR integration.
|
||||
|
||||
Nightcap is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
The standard video profile is **native 2160 × 2160 per eye**, **72 Hz**,
|
||||
**SMAA High** and **Performance** foveation. Frame rate depends on the Roblox
|
||||
experience and its graphics settings; 72 FPS is the target.
|
||||
|
||||
## Features
|
||||
|
||||
- Native ARM64 execution through Nightcap's Android compatibility layer.
|
||||
- Stereo OpenXR rendering, head tracking, VR controllers, haptics and an
|
||||
in-headset keyboard.
|
||||
- Direct eye tracking with a full-detail gaze region and gradual reductions in
|
||||
peripheral shading quality.
|
||||
- Adjustable gaze radius and Quality, Balanced and Performance foveation modes.
|
||||
- Gaze-only FXAA, SMAA Low and SMAA High anti-aliasing.
|
||||
- Native-resolution rendering with a 2160 × 2160 per-eye recommended default.
|
||||
- Roblox's internal frame-rate cap follows the selected headset refresh rate.
|
||||
- Renderer and session handling across menu, game and game-to-game transitions.
|
||||
- Native microphone capture bridge for Roblox voice chat.
|
||||
- One **Nightcap Roblox VR** Steam entry with the original Nightcap icon and an
|
||||
optional desktop mirror. The mirror is off by default on Steam Frame.
|
||||
- Simple **Foveated Rendering**, **Gaze sharp radius** and **Anti Aliasing**
|
||||
controls in SteamVR's Video settings.
|
||||
- Optional in-headset performance overlay with actual game FPS, headset Hz,
|
||||
frame time, GPU time and current per-eye resolution.
|
||||
|
||||
## Install
|
||||
|
||||
Install the native ARM64 build from [Releases](https://github.com/InstanceBrick/nightcap/releases),
|
||||
or build it on the Frame using the [installation guide](docs/INSTALL.md).
|
||||
The ARM64 package contains Nightcap and its open-source runtime dependencies.
|
||||
Roblox is downloaded separately on the device. Roblox binaries, account data,
|
||||
SteamVR files and the Frame's graphics drivers are not included in the package.
|
||||
|
||||
Current ARM64 client support: **2.742.1059 (3222)** and **2.736.1408 (2998)**.
|
||||
|
||||
- [Installation and updates](docs/INSTALL.md)
|
||||
- [Video, input and microphone settings](docs/SETTINGS.md)
|
||||
- [Building and source layout](docs/DEVELOPMENT.md)
|
||||
|
||||
## About this fork
|
||||
|
||||
Nightcap is a fork of [komaruworld/mocktail](https://github.com/komaruworld/mocktail)
|
||||
with extra fixes for playing on a PC. The program and config paths keep the
|
||||
`mocktail` name so upstream fixes still apply cleanly. The app ID is
|
||||
`io.github.CoderDayton.nightcap`, so Nightcap and upstream can be installed
|
||||
side by side.
|
||||
|
||||
- **Keeps up with Roblox.** New Roblox versions work soon after they come out.
|
||||
- **Plays more games.** Nightcap answers a server check that some experiences
|
||||
use, so you can join and stay in those too.
|
||||
- **Voice chat that keeps working.** Your microphone still works after a
|
||||
Roblox update.
|
||||
- **Logging in works.** Verification checks and captchas finish instead of
|
||||
hanging.
|
||||
- **The current Roblox look.** The new home screen and side menu, like on
|
||||
Windows.
|
||||
- **Your own cursors, fonts and skies.** Drop files in
|
||||
`~/.local/share/mocktail/asset_overlay` and Nightcap uses them. The layout
|
||||
matches Sober's `asset_overlay` folder.
|
||||
- **Smoother mouse and a cooler PC.** Optional raw mouse input, and less CPU
|
||||
use while idle.
|
||||
|
||||
Everything else works the same as upstream. See
|
||||
[`textures/`](textures/README.md) for custom textures, a ready-made crosshair
|
||||
and the asset overlay folder, the [roadmap](docs/ROADMAP.md) for what is
|
||||
coming next, the [changelog](CHANGELOG.md) for what each release changed,
|
||||
[performance](docs/PERFORMANCE.md) for CPU load, heat and the knobs that change
|
||||
them, and the [FAQ](FAQ.md) for Discord RPC, logs, and VR.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap
|
||||
```
|
||||
|
||||
Updates arrive through `flatpak update`. Nightcap is not on Flathub yet.
|
||||
|
||||
## AppImage
|
||||
|
||||
Grab the AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest),
|
||||
make it executable, and run it:
|
||||
|
||||
```bash
|
||||
chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
## Distribution packages
|
||||
|
||||
Each release also carries a DEB, an RPM, and a pacman package, with a
|
||||
`.sha256` beside every file on the
|
||||
[releases page](https://github.com/CoderDayton/nightcap/releases/latest):
|
||||
|
||||
```bash
|
||||
sudo apt install ./nightcap_<version>_amd64.deb
|
||||
sudo dnf install ./nightcap-<version>-1.x86_64.rpm
|
||||
sudo pacman -U nightcap-<version>-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
They install `/usr/bin/mocktail`, so they conflict with upstream's `mocktail`
|
||||
package.
|
||||
|
||||
On Arch Linux, [`nightcap-git`](https://aur.archlinux.org/packages/nightcap-git)
|
||||
is on the AUR. It builds the current `main` branch and is maintained by
|
||||
[@dim-ghub](https://github.com/dim-ghub):
|
||||
|
||||
```bash
|
||||
yay -S nightcap-git
|
||||
```
|
||||
|
||||
Nightcap is in no other distribution repository.
|
||||
|
||||
Or build and install from source using the steps under
|
||||
[Building](#building).
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
|
||||
```
|
||||
|
||||
The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Nightcap. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
A leftover `.previous_good-recovery.json` in the payload store marks pending
|
||||
recovery after a publication or restoration failure and blocks further payload
|
||||
publication and garbage collection. Check `current.json` and `previous_good.json`
|
||||
against the saved recovery state and fix the underlying problem before manually
|
||||
removing the marker.
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## FFlag overrides
|
||||
|
||||
Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
`~/.config/mocktail/fflags.json`). Nightcap applies it on the next launch.
|
||||
|
||||
PC profiles use the legacy Charts page by default to avoid the blank screen
|
||||
caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
`FFlagLuaAppChartsAppPage` override takes precedence over this default.
|
||||
|
||||
PC profiles also set `FIntFlyoutTestVariantRollout12` to `2`, which shows the
|
||||
desktop side panel menu. An explicit override takes precedence.
|
||||
|
||||
PC profiles compile the app's menus to native code with
|
||||
`FFlagDebugLuauCodegenAll`, and turn it off while you are in a game. Setting
|
||||
it yourself turns the switching off and your value applies everywhere. See
|
||||
[docs/PERFORMANCE.md](docs/PERFORMANCE.md#native-code-in-the-menus).
|
||||
|
||||
```json
|
||||
{
|
||||
"FFlagExample": "True",
|
||||
"DFIntExample": "120"
|
||||
}
|
||||
```
|
||||
## Settings
|
||||
|
||||
Edit `~/.config/mocktail/config.yaml`. Useful options:
|
||||
|
||||
- `graphics.frame_rate_limit: display` to unlock FPS to your monitor's rate.
|
||||
- `graphics.vsync: off` for the lowest lag, at the cost of tearing.
|
||||
- `vr.frame_rate_limit` sets the cap in VR. It defaults to `unlimited`, so the
|
||||
headset's refresh rate paces frames; use `-1` for Roblox's 60 FPS cap.
|
||||
- `graphics.backend: opengl` if Vulkan does not work on your GPU.
|
||||
|
||||
## Building
|
||||
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Nightcap runs
|
||||
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
|
||||
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
|
||||
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, libpng,
|
||||
minizip, Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and
|
||||
WebKitGTK 6.0.
|
||||
|
||||
See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instructions.
|
||||
|
||||
<details>
|
||||
<summary>Ubuntu 26.04+</summary>
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libpng-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev zlib1g-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Arch Linux</summary>
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
curl openssl nlohmann-json libyaml libpng libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
libplacebo vulkan-headers vulkan-icd-loader zlib
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora 44+</summary>
|
||||
|
||||
```bash
|
||||
sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel libpng-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel \
|
||||
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
|
||||
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
|
||||
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel
|
||||
```
|
||||
</details>
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap
|
||||
make build
|
||||
./build/mocktail
|
||||
```
|
||||
|
||||
To install it for your user, with an app menu entry and Roblox link handling:
|
||||
|
||||
```bash
|
||||
make install PREFIX=~/.local
|
||||
```
|
||||
|
||||
Use `sudo make install` instead to install system-wide under `/usr`. To make
|
||||
Roblox website links open the build tree copy without installing, run
|
||||
`make register-url-handler`.
|
||||
|
||||
To build and install the Flatpak from source, install `flatpak-builder` and run
|
||||
`make flatpak`. It builds with 2 parallel jobs to keep memory use low. Pass
|
||||
`FLATPAK_JOBS=N` to `make flatpak`, or set `MOCKTAIL_FLATPAK_JOBS=N` for
|
||||
`scripts/build_flatpak.sh`, to change that.
|
||||
|
||||
To run the same checks as CI before each commit and push, install
|
||||
[lefthook](https://github.com/evilmartians/lefthook) and run once:
|
||||
|
||||
```bash
|
||||
lefthook install
|
||||
```
|
||||
|
||||
## Experimental VR
|
||||
|
||||
Experimental OpenXR VR support on Linux `x86_64` using WiVRn or SteamVR/ALVR.
|
||||
|
||||
VR works on these Roblox builds. On any other build, `--vr` stops at start-up
|
||||
and names them:
|
||||
|
||||
| Build | Roblox version |
|
||||
| --- | --- |
|
||||
| 2998 | 2.736.1408 |
|
||||
| 3092 | 2.738.1397 |
|
||||
| 3222 | 2.742.1059 |
|
||||
|
||||
### Build with VR
|
||||
|
||||
VR support is compiled in when the OpenXR SDK submodule is checked out
|
||||
(`MOCKTAIL_ENABLE_VR` defaults to `AUTO`). Without the submodule the build
|
||||
skips VR. Pass `-DMOCKTAIL_ENABLE_VR=ON` to make a missing submodule an error,
|
||||
or `OFF` to leave VR out:
|
||||
|
||||
```bash
|
||||
git submodule update --init third_party/OpenXR-SDK
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j4
|
||||
```
|
||||
|
||||
VR stays off at runtime until you pass `--vr` or set `vr.mode: on` in
|
||||
`config.yaml`. `vr.mode: auto` starts in VR only when SteamVR is running with a
|
||||
headset connected.
|
||||
|
||||
### Run
|
||||
|
||||
Start WiVRn or SteamVR/ALVR and connect your headset first:
|
||||
|
||||
```bash
|
||||
./build/mocktail --vr
|
||||
```
|
||||
|
||||
The OpenXR runtime is detected automatically. To pick one manually:
|
||||
|
||||
```bash
|
||||
MOCKTAIL_VR_RUNTIME=wivrn ./build/mocktail --vr
|
||||
# or
|
||||
MOCKTAIL_VR_RUNTIME=alvr MOCKTAIL_GRAPHICS_BACKEND=direct-vulkan ./build/mocktail --vr
|
||||
```
|
||||
|
||||
If you previously set `XR_RUNTIME_JSON`, unset it before switching runtimes.
|
||||
Use `--no-vr` for desktop mode.
|
||||
|
||||
A keyboard opens in the headset when a text box takes focus. The rest is in
|
||||
[docs/VR.md](docs/VR.md): menus and typing, every VR environment variable,
|
||||
adding a Roblox build, and testing without a headset.
|
||||
|
||||
## Known limitations
|
||||
|
||||
Nightcap does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Nightcap to fail to start or crash during runtime.
|
||||
Run Nightcap without `hardened_malloc` enabled.
|
||||
The `steam-frame` branch is the Steam Frame edition of the `vr` branch of
|
||||
[CoderDayton/nightcap](https://github.com/CoderDayton/nightcap), itself a fork of
|
||||
[komaruworld/mocktail](https://github.com/komaruworld/mocktail). It adds the native
|
||||
ARM64 VR bridge and Frame rendering, input, audio and Steam integration changes.
|
||||
The original Nightcap artwork is retained. Internal executable and configuration
|
||||
names keep `mocktail` for compatibility with the upstream source.
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE). Third-party components keep their own licenses.
|
||||
[Apache License 2.0](LICENSE). Original copyright notices and third-party
|
||||
licenses remain in their source directories. See [third-party credits](THIRD_PARTY.md).
|
||||
|
||||
## Support
|
||||
|
||||
Support this fork by giving it a star or with cryptocurrency:
|
||||
|
||||
- LTC: `LdABR2ELRYrUESWZWrEk1uNx38gHHMuHUU`
|
||||
- SOL: `tjeDnPoWeyW8zfTf8c5CVAm5QTV8vFPaPouaScAdwfT`
|
||||
|
||||
Support the upstream project, which does most of the heavy lifting:
|
||||
|
||||
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
|
||||
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
|
||||
Nightcap is an independent community project and is not affiliated with Roblox
|
||||
Corporation or Valve.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Credits and third-party components
|
||||
|
||||
This edition includes modifications for Steam Frame ARM64 and is based on:
|
||||
|
||||
- [Nightcap](https://github.com/CoderDayton/nightcap), including vii's changes to
|
||||
Mocktail and the upstream `vr` branch.
|
||||
- [Mocktail](https://github.com/komaruworld/mocktail), by the Mocktail project
|
||||
authors.
|
||||
|
||||
The Steam Frame edition incorporates the upstream VR tree through
|
||||
[`753a3f81`](https://github.com/CoderDayton/nightcap/commit/753a3f8106e4d7fdafea457601a5ff238e77c2d0)
|
||||
(15 October 2026), including GPU-ordered presentation, asynchronous eye and
|
||||
mirror copies, cached JNI method dispatch, headset detection and the JNI haptic
|
||||
protocol. ARM64 client contracts and Frame gaze AA remain platform-specific.
|
||||
|
||||
The project is distributed under the [Apache License 2.0](LICENSE). Existing
|
||||
copyright and modification notices are retained in the source.
|
||||
|
||||
Third-party components retain their own licenses and notices, including the
|
||||
Android/Bionic linker sources under `third_party/mcpelauncher-linker`, the pinned
|
||||
OpenXR SDK, Vulkan headers and libjnivm submodules, SMAA under `src/vr/smaa`, AMD FidelityFX FSR 1 under `src/vr/fsr`
|
||||
([MIT license](src/vr/fsr/LICENSE.txt)),
|
||||
AMD FidelityFX SDK 1.1.4's FSR 3.1.4 upscaler, Optical Flow component and Vulkan
|
||||
backend under `third_party/fidelityfx`
|
||||
([MIT license](third_party/fidelityfx/LICENSE.txt)), and
|
||||
the OpenVR interface header under `tools/frame_video_settings`. The original
|
||||
Nightcap icon and artwork are retained from the upstream project.
|
||||
|
||||
Roblox client binaries and Valve SteamVR dashboard assets are not included.
|
||||
The client is downloaded separately and the Video settings tools read dashboard
|
||||
files from the user's installed SteamVR copy.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1009 KiB |
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cmake -S /work -B /work/build-frame-arm64 -G Ninja -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON -DMOCKTAIL_ENABLE_FSR3=OFF -DMOCKTAIL_VR_DIAGNOSTICS=OFF -DBUILD_TESTING=OFF -DMOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER=OFF -DCMAKE_INSTALL_PREFIX=/opt/nightcap-vr-arm64
|
||||
cmake --build /work/build-frame-arm64 -j3
|
||||
cmake --install /work/build-frame-arm64
|
||||
c++ -std=c++17 -O2 /work/tools/frame_video_settings/nightcap-vr-settings.cc -ldl -o /opt/nightcap-vr-arm64/bin/nightcap-vr-settings
|
||||
printf 'NIGHTCAP_ARM64_BUILD_COMPLETE\n'
|
||||
+14
-9
@@ -3,6 +3,8 @@
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
option(MOCKTAIL_ENABLE_FSR3 "Build the optional temporal upscaler SDK" OFF)
|
||||
|
||||
# AUTO builds OpenXR support when the third_party/OpenXR-SDK submodule is
|
||||
# checked out and skips it otherwise, so source packages that do not fetch
|
||||
# submodules still build. ON requires the submodule; OFF never builds it.
|
||||
@@ -26,6 +28,9 @@ else()
|
||||
endif()
|
||||
|
||||
add_library(mocktail_vr STATIC
|
||||
src/vr/gaze_aa.cc
|
||||
src/vr/fsr_upscaler.cc
|
||||
src/vr/fsr3_scene_capture.cc
|
||||
src/vr/diagnostics.cc
|
||||
src/vr/headset_detection.cc
|
||||
src/vr/openxr_backend_mode.cc
|
||||
@@ -69,8 +74,14 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
"${CMAKE_BINARY_DIR}/third_party/OpenXR-SDK" EXCLUDE_FROM_ALL)
|
||||
endfunction()
|
||||
mocktail_add_openxr_sdk()
|
||||
if(MOCKTAIL_ENABLE_FSR3)
|
||||
add_subdirectory(third_party/fidelityfx)
|
||||
target_sources(mocktail_vr PRIVATE src/vr/fsr3_upscaler.cc src/vr/fsr3_gaze_transport.cc)
|
||||
target_link_libraries(mocktail_vr PRIVATE nightcap_fsr3)
|
||||
target_compile_definitions(mocktail_vr PUBLIC MOCKTAIL_ENABLE_FSR3=1)
|
||||
endif()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_probe.cc src/vr/openxr_preview.cc
|
||||
src/vr/openxr_backend.cc src/vr/gles_transport.cc src/vr/xr_actions.cc
|
||||
src/vr/openxr_backend.cc src/vr/xr_foveation_maps.cc src/vr/gles_transport.cc src/vr/xr_actions.cc
|
||||
src/vr/virtual_keyboard_raster.cc)
|
||||
# The in-headset keyboard draws its labels with the same SDL3_ttf and
|
||||
# fontconfig stack as the TextBox overlay.
|
||||
@@ -83,7 +94,7 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
# Error unwinding is confined to these native session implementations; their
|
||||
# public entry points catch errors before returning to the guest runtime.
|
||||
set_source_files_properties(src/vr/openxr_preview.cc src/vr/openxr_backend.cc
|
||||
src/vr/xr_actions.cc
|
||||
src/vr/xr_actions.cc src/vr/xr_foveation_maps.cc
|
||||
PROPERTIES COMPILE_OPTIONS -fexceptions)
|
||||
target_compile_definitions(mocktail_vr PRIVATE XR_USE_GRAPHICS_API_VULKAN)
|
||||
option(MOCKTAIL_VR_DIAGNOSTICS
|
||||
@@ -93,16 +104,10 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
endif()
|
||||
set_source_files_properties(src/vr/openxr_backend.cc PROPERTIES COMPILE_DEFINITIONS "XR_USE_GRAPHICS_API_OPENGL_ES;XR_USE_PLATFORM_EGL")
|
||||
target_include_directories(mocktail_vr PRIVATE ${MOCKTAIL_EGL_INCLUDE_DIR} ${MOCKTAIL_GLES3_INCLUDE_DIR})
|
||||
# SMAA's area and search lookup textures for vr.anti_aliasing: smaa.
|
||||
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/third_party/smaa/Textures/AreaTex.h")
|
||||
message(FATAL_ERROR
|
||||
"Initialize SMAA: git submodule update --init third_party/smaa")
|
||||
endif()
|
||||
target_include_directories(mocktail_vr PRIVATE "${CMAKE_SOURCE_DIR}/third_party/smaa")
|
||||
target_link_libraries(mocktail_vr PRIVATE OpenXR::openxr_loader Vulkan::Headers ${CMAKE_DL_LIBS})
|
||||
else()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_disabled.cc
|
||||
src/vr/xr_actions_disabled.cc)
|
||||
src/vr/xr_actions_disabled.cc src/vr/xr_foveation_maps_disabled.cc)
|
||||
endif()
|
||||
|
||||
if(MOCKTAIL_ENABLE_VR)
|
||||
|
||||
@@ -28,7 +28,7 @@ device: pc-windows-11
|
||||
runtime:
|
||||
# Boolean (default: false): start without a visible SDL window.
|
||||
headless: false
|
||||
# String (optional): use a specific x86_64 libroblox.so instead of the
|
||||
# String (optional): use a specific native-architecture libroblox.so instead of the
|
||||
# active verified payload.
|
||||
# roblox_library: /absolute/path/to/libroblox.so
|
||||
|
||||
@@ -54,18 +54,21 @@ graphics:
|
||||
|
||||
# vr:
|
||||
# String (default: off): experimental VR through OpenXR on supported Roblox
|
||||
# builds. off always starts on the desktop. on always starts in VR, like
|
||||
# --vr. auto starts in VR when SteamVR is already running with a connected
|
||||
# builds (ARM64: 2998 and 3222). off starts on the desktop; on starts in
|
||||
# VR like --vr. auto starts in VR when SteamVR is already running with a connected
|
||||
# headset (ALVR included), and on the desktop otherwise. auto never starts
|
||||
# SteamVR. --vr and --no-vr override this for one launch. The older
|
||||
# enabled: true/false still works as on/off; set one or the other.
|
||||
# Standalone OpenXR test scene: mocktail-vr-probe --scene.
|
||||
# mode: off
|
||||
# Boolean (default: true): also show the left-eye VR view in the desktop
|
||||
# window. Restart Nightcap after changing this setting. Steam Frame
|
||||
# uses its Video settings Desktop Mirror control, off by default.
|
||||
# desktop_mirror: true
|
||||
# String (default: fxaa): smooth jagged edges in the headset. Roblox draws
|
||||
# the VR eyes without anti-aliasing, so each eye is filtered on its way to
|
||||
# the headset. fxaa is the cheapest. smaa finds edges more precisely and
|
||||
# keeps text and fine detail sharper, at a little more GPU time. off sends
|
||||
# the eyes as rendered.
|
||||
# the VR eyes without anti-aliasing; fxaa filters each eye on its way to the
|
||||
# headset. The Steam Frame profile uses its separate gaze AA control.
|
||||
# off sends the eyes as rendered.
|
||||
# anti_aliasing: fxaa
|
||||
# Optional string or integer: frame-rate limit used instead of
|
||||
# graphics.frame_rate_limit while VR is active. Same values: -1 (Roblox's
|
||||
|
||||
@@ -77,7 +77,20 @@
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching."
|
||||
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching.",
|
||||
"fmod_output_device_bridge": {
|
||||
"vtable_rva": "0x632aeb0",
|
||||
"string_constructor_rva": "0x1d5a6b0",
|
||||
"count_method_rva": "0x2f96540",
|
||||
"info_method_rva": "0x2f96600",
|
||||
"current_method_rva": "0x2f965a0",
|
||||
"select_method_rva": "0x2f962d4",
|
||||
"input_count_method_rva": "0x2f968b4",
|
||||
"input_info_method_rva": "0x2f968c4",
|
||||
"input_current_method_rva": "0x2f9697c",
|
||||
"input_select_method_rva": "0x2f96088",
|
||||
"vtable_layout_version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
@@ -142,6 +155,40 @@
|
||||
"emulator_flag_storage_rva": "0x73f4690",
|
||||
"device_create_framebuffer_vtable_offset": "0x188"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version_name": "2.742.1059",
|
||||
"version_code": 3222,
|
||||
"abi": "arm64-v8a",
|
||||
"elf_build_id": "25aa7fd31008a744c3c99f717e7d16d81d371d12",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "Verified on Steam Frame 2026-10-15: signed ARM64 3222 payload, exact allocator/bootstrap and reordered constructor roles, VR state/pointer/reflection/framebuffer contracts and layout-2 FMOD bridge; native startup, two distinct eye bindings at 2592x2592, real OpenXR projection and controlled experience transitions. Worn-headset quality and remote voice audibility remain unconfirmed.",
|
||||
"fmod_output_device_bridge": {
|
||||
"vtable_rva": "0x6569770",
|
||||
"string_constructor_rva": "0x1e47180",
|
||||
"count_method_rva": "0x2c35714",
|
||||
"info_method_rva": "0x2c357d4",
|
||||
"current_method_rva": "0x2c35774",
|
||||
"select_method_rva": "0x2c34f18",
|
||||
"input_count_method_rva": "0x2c35d58",
|
||||
"input_info_method_rva": "0x2c35d68",
|
||||
"input_current_method_rva": "0x2c361a0",
|
||||
"input_select_method_rva": "0x2c34c80",
|
||||
"vtable_layout_version": 2
|
||||
},
|
||||
"vr_debug_device_bridge": {
|
||||
"vtable_rva": "0x67b6688",
|
||||
"constructor_rva": "0x4e5558c",
|
||||
"state_getter_rva": "0x4e55964",
|
||||
"eye_getter_rva": "0x4e55958",
|
||||
"eye_initializer_rva": "0x4e55974",
|
||||
"emulator_flag_storage_rva": "0x6c0d1b0",
|
||||
"device_create_framebuffer_vtable_offset": "0x188"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
version: 1
|
||||
device: pc-windows-11
|
||||
|
||||
graphics:
|
||||
backend: direct-vulkan
|
||||
vsync: off
|
||||
|
||||
vr:
|
||||
enabled: true
|
||||
desktop_mirror: false
|
||||
frame_rate_limit: unlimited
|
||||
|
||||
audio:
|
||||
output_device: default
|
||||
input_device: default
|
||||
|
||||
updates:
|
||||
automatic: false
|
||||
launch_after_update: false
|
||||
source: auto
|
||||
@@ -1,129 +0,0 @@
|
||||
# Benchmarking
|
||||
|
||||
A pull request that claims a speedup shows before and after numbers taken
|
||||
the way this guide describes. Numbers from different machines are not
|
||||
compared with each other. Each run is compared with a run on the same
|
||||
machine.
|
||||
|
||||
## Two layers, two tools
|
||||
|
||||
Roblox measures its own work. Nightcap measures the layer between Roblox and
|
||||
your driver.
|
||||
|
||||
| Layer | What it covers | Tool |
|
||||
| --- | --- | --- |
|
||||
| Roblox | Scripts, physics, rendering jobs, network | Roblox's stats overlays, Shift+F4 and Shift+F5 in game |
|
||||
| Nightcap | Vulkan adapter: present, swapchain and GPU waits, submits, ETC2 texture decode, pipeline and shader creation | `--profile` |
|
||||
|
||||
A Nightcap change should move the `--profile` numbers. If it only moves the
|
||||
Roblox overlay numbers, say so in the pull request, since the cause is then
|
||||
less direct.
|
||||
|
||||
## Recording a trace
|
||||
|
||||
```sh
|
||||
mocktail --profile ~/nightcap-before.json
|
||||
```
|
||||
|
||||
The Vulkan adapter writes a Chrome-format trace to that file, a few times a
|
||||
second. The file is valid JSON after every write, so a session that crashes
|
||||
or is killed still leaves a readable trace. Exit from the Roblox menu when you are done. Open the file
|
||||
in [ui.perfetto.dev](https://ui.perfetto.dev) or `chrome://tracing`.
|
||||
|
||||
Expect about 130 bytes per event, around 15 MB per minute at 240 FPS.
|
||||
Profiling is off unless `--profile` is given, and `MOCKTAIL_PROFILE_TRACE`
|
||||
does the same thing as the flag.
|
||||
|
||||
### What the trace contains
|
||||
|
||||
Each slice is named after the call it times, on the thread that made it.
|
||||
|
||||
| Category | Slices | Arguments |
|
||||
| --- | --- | --- |
|
||||
| `present` | `vkQueuePresentKHR`: the whole adapter present, including the text overlay | `result` |
|
||||
| `present` | `host present`: the host driver's present, nested inside the slice above | |
|
||||
| `wait` | `vkAcquireNextImageKHR`, `vkAcquireNextImage2KHR`, `vkWaitForFences`, `vkWaitSemaphores`, `vkWaitSemaphoresKHR`, `vkQueueWaitIdle`, `vkDeviceWaitIdle` | |
|
||||
| `submit` | `vkQueueSubmit`, `vkQueueSubmit2`, `vkQueueSubmit2KHR` | `submits` |
|
||||
| `texture` | `etc2 decode`: CPU decode of ETC2 textures. On a device with a timeline semaphore this runs on the decode dispatcher, not the submitting thread | `uploads`, `compressed_bytes`, `decoded_bytes` |
|
||||
| `texture` | `etc2 gather`: the part of an asynchronous upload that stays on the submitting thread. Absent when the decode ran inline | `uploads`, `compressed_bytes`, `decoded_bytes`, `ticket` |
|
||||
| `texture` | `etc2 staging create`: creating a staging block for ETC2 uploads, when no block has room left | `bytes` |
|
||||
| `texture` | `etc2 staging destroy`: destroying empty staging blocks beyond the four kept idle | `buffers` |
|
||||
| `texture` | `etc2 release wait`: a command buffer reset waiting for its uploads' decode. Only waits of 0.1 ms or more are recorded | `ticket` |
|
||||
| `memory` | `vkAllocateMemory` | `bytes`, `type` (memory type index), `result` |
|
||||
| `memory` | `vkFreeMemory`, `vkUnmapMemory`: both include waiting for ETC2 decodes that still read that memory | |
|
||||
| `memory` | `vkMapMemory`, `vkCreateImage`, `vkDestroyImage` | |
|
||||
| `pipeline` | `vkCreateGraphicsPipelines`, `vkCreateComputePipelines` | `count`, `cache` (1 when a pipeline cache was passed), `result` |
|
||||
| `pipeline` | `vkCreateShaderModule` | `bytes`, `result` |
|
||||
| `pipeline` | `vkCreatePipelineCache` | `initial_bytes`, `result` |
|
||||
| `pump` | `nativeCallMessagesFromMainThread`: the engine's main-thread step, on the host main thread. Only calls of 20 µs or more are recorded; the empty polls between them are not. | `cpu_us`: CPU time the main thread used during the call. Far below the slice's duration means the step spent it waiting |
|
||||
|
||||
The `frame interval (ms)` counter is the time between successive adapter
|
||||
presents. `vkQueuePresentKHR` time minus `host present` time is the adapter's
|
||||
own cost per frame.
|
||||
|
||||
Two more counters cover the same interval. `present thread cpu (ms)` is the
|
||||
CPU time the thread that presents used in it, and is absent for a frame
|
||||
presented from a different thread than the one before. `process cpu (ms)` is
|
||||
the CPU time of every thread together, so dividing it by the frame interval
|
||||
gives the number of cores busy. A long frame with little present-thread CPU
|
||||
was spent waiting.
|
||||
|
||||
`emulated texture (MB)` is the VRAM held by the decoded images that stand in
|
||||
for live ETC2/EAC textures, over every mip level and layer, upscaled where
|
||||
small-texture upscale applies. `compressed texture (MB)` is the size the same
|
||||
textures would take as ETC2/EAC. Both change only when such an image is
|
||||
created or destroyed. `summarize_profile_trace.py` prints each one's peak.
|
||||
|
||||
## The benchmark runs
|
||||
|
||||
Run every scenario before and after the change, on the same build type
|
||||
(`make build`), with the same `config.yaml`. Close other GPU-heavy apps.
|
||||
Record `graphics.frame_rate_limit` and `graphics.vsync` in the pull request,
|
||||
because a frame cap hides frame time gains.
|
||||
|
||||
Record `~/.config/gamemode.ini` too, and do not change it between the before
|
||||
and after runs. `desiredgov` there decides the CPU governor for the whole
|
||||
session and therefore which engine pump mode is chosen, which moves process
|
||||
CPU by more than most changes under test. See
|
||||
[PERFORMANCE.md](PERFORMANCE.md).
|
||||
|
||||
Frame interval p99 varies by 2 ms or more between identical runs, so a
|
||||
difference that size is not a result. Three runs per scenario is the minimum
|
||||
for any frame-time claim; slice totals for a specific subsystem are far less
|
||||
noisy and can be read from fewer.
|
||||
|
||||
| Scenario | How to start | What to do | What it shows |
|
||||
| --- | --- | --- | --- |
|
||||
| Home screen, cold | `mocktail --profile home-cold.json` with an empty shader cache (below) | Wait 30 s on the home screen, exit | First-load stutter: `pipeline` and `texture` slices |
|
||||
| Tower of Hell | `mocktail --profile toh.json --launch-uri "roblox://experiences/start?placeId=1962086868"` | Stand still at spawn for 60 s, exit | Steady-state frame time on a light scene |
|
||||
| Murder Mystery 2 | `mocktail --profile mm2.json --launch-uri "roblox://experiences/start?placeId=142823291"` | Stay in the lobby for 60 s, exit | Frame time with many players and a busier scene |
|
||||
|
||||
For the cold run, point the driver's shader cache at an empty directory, so
|
||||
your normal cache is left alone:
|
||||
|
||||
```sh
|
||||
cache=$(mktemp -d)
|
||||
MESA_SHADER_CACHE_DIR="$cache" __GL_SHADER_DISK_CACHE_PATH="$cache" \
|
||||
mocktail --profile home-cold.json
|
||||
```
|
||||
|
||||
`MESA_SHADER_CACHE_DIR` applies to Mesa drivers (AMD, Intel) and
|
||||
`__GL_SHADER_DISK_CACHE_PATH` to the NVIDIA driver.
|
||||
|
||||
Run each scenario three times and keep the median run, since one run can
|
||||
catch a background stall.
|
||||
|
||||
## Comparing runs
|
||||
|
||||
```sh
|
||||
scripts/summarize_profile_trace.py toh-before.json toh-after.json
|
||||
```
|
||||
|
||||
This prints a Markdown table with one column per trace: frame count, frame
|
||||
interval mean, p50, p95, p99 and max, then count, total, mean, p99 and max
|
||||
for every slice name, largest total first. Paste the table into the pull
|
||||
request and attach both traces.
|
||||
|
||||
For frame time, compare p95 and p99 as well as the mean. Stutter shows up in
|
||||
the tail first. For first-load work, compare the `total (ms)` rows of the
|
||||
`pipeline` and `texture` slices from the cold run.
|
||||
@@ -0,0 +1,203 @@
|
||||
# Building and source layout
|
||||
|
||||
The [installation guide](INSTALL.md) covers a complete source build on Steam
|
||||
Frame. The container recipe uses Fedora 44 ARM64 with CMake, Ninja, SDL3,
|
||||
OpenXR, Vulkan, WebKitGTK and Nightcap's other native dependencies.
|
||||
The Frame build enables OpenXR and the ARM64 bridge, disables runtime
|
||||
diagnostics, and uses native rendering. Fedora's `glslang` and `spirv-tools`
|
||||
packages are included for shader generation and validation. The ARM64 workflow
|
||||
builds the same container recipe and checks the package before export.
|
||||
|
||||
## Upstream source
|
||||
|
||||
This branch includes Nightcap's `vr` additions through
|
||||
[`adccf4e4841275bce85d0330013367e2c124bd10`](https://github.com/CoderDayton/nightcap/commit/adccf4e4841275bce85d0330013367e2c124bd10),
|
||||
including all changes from its `main` branch at that revision. The update adds
|
||||
emoji fallback fonts for native text fields, decoded/compressed emulated-texture
|
||||
byte counters, menu-only native Luau compilation, and queued game switching.
|
||||
Menu compilation is enabled only after validating the native flag on ARM64;
|
||||
explicit client settings remain in control.
|
||||
|
||||
Frame already uses the same reference SMAA revision as this update. Its existing
|
||||
gaze AA path is retained without adding a second full-eye filter. Frame's native
|
||||
2160 × 2160 rendering, 72 Hz profile, simple Video controls, controllers, audio
|
||||
and foveation integration remain the platform defaults.
|
||||
|
||||
## Build scripts
|
||||
|
||||
`build-frame-nightcap.sh` configures a Release build at `/work/build-frame-arm64`
|
||||
and installs to `/opt/nightcap-vr-arm64`. VR is enabled and runtime diagnostics
|
||||
are disabled. It also builds the Steam application identity helper.
|
||||
|
||||
For incremental development in an existing Frame image, with Nightcap closed:
|
||||
|
||||
```bash
|
||||
podman run --arch arm64 --name nightcap-frame-development \
|
||||
--cpus 3 --memory 8g --security-opt label=disable \
|
||||
-v "$PWD:/work" --entrypoint /bin/bash \
|
||||
localhost/nightcap-vr-arm64:frame-foveated -lc \
|
||||
'bash /work/build-frame-nightcap.sh && bash /work/test-arm64-bridge.sh'
|
||||
```
|
||||
|
||||
The test script runs focused ARM64 bridge, pose, controller, lifecycle, JNI,
|
||||
audio and host ABI tests, including the native assembly return-value fixture.
|
||||
It also builds the GPU test tools. Build and test commands share one CMake tree;
|
||||
run them sequentially.
|
||||
|
||||
## Main components
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `src/vr/` | OpenXR lifecycle, gaze, stereo eye transport, controllers, AA and foveation maps |
|
||||
| `src/vr/roblox_vr_arm64_build_contracts.h` | Selects exact ARM64 client contracts |
|
||||
| `src/vr/roblox_vr_arm64_contract*.h` | Client-specific ARM instruction and object layout contracts |
|
||||
| `src/audio/` | Audio playback and microphone recording bridges |
|
||||
| `src/compat/host_abi_profile.cc` | Client startup, allocator and native host ABI profiles |
|
||||
| `src/graphics/vulkan_foveation_adapter.inc` | Scene rendering integration for density maps |
|
||||
| `tools/steam_frame/` | Frame launcher, Steam shortcut and Video settings integration |
|
||||
| `tools/frame_video_settings/` | OpenVR settings, refresh-rate and timing helpers |
|
||||
| `config/roblox_compatibility.json` | Supported client identities and compatibility metadata |
|
||||
| `config/steam-frame.example.yaml` | Initial Frame runtime settings |
|
||||
|
||||
## Client compatibility
|
||||
|
||||
The ARM64 bridge supports these client identities:
|
||||
|
||||
| Version | Build | ELF build ID |
|
||||
| --- | --- | --- |
|
||||
| 2.742.1059 | 3222 | `25aa7fd31008a744c3c99f717e7d16d81d371d12` |
|
||||
| 2.736.1408 | 2998 | `ed2b035768b1513561d0e6ea7f9bbec35b0e60e5` |
|
||||
|
||||
New clients can change native methods, virtual tables, object fields and startup
|
||||
ordering. Add matching ARM64 contracts and host/audio profiles before enabling a
|
||||
new client. Keep the existing signature and build identity checks in place.
|
||||
|
||||
The timing helper in `tools/frame_video_settings/frame_timings.cc` reports the
|
||||
scene process, dashboard visibility, headset activity and compositor throttling
|
||||
alongside frame times. Compare the same view in the same experience while the
|
||||
headset is displaying projection frames. Standby or desktop-only frames do not
|
||||
measure active VR throughput.
|
||||
|
||||
Shader sources and their generated SPIR-V header are included. Regenerating the
|
||||
AA shaders with `tools/compile_gaze_aa.py` additionally needs `glslangValidator`.
|
||||
The SMAA sources retain their upstream license in `src/vr/smaa/`.
|
||||
|
||||
## Native rendering
|
||||
|
||||
The shipping Frame profile uses native rendering with gaze anti-aliasing.
|
||||
MSAA and upscaling controls are absent from the Frame Video settings. The build
|
||||
sets `MOCKTAIL_ENABLE_FSR3=OFF`, so the FidelityFX SDK is not linked into the
|
||||
Frame runtime. Reference upscaler sources and their licenses remain in the tree.
|
||||
|
||||
To regenerate the gaze AA shaders after changing them:
|
||||
|
||||
```sh
|
||||
python3 tools/compile_gaze_aa.py .
|
||||
```
|
||||
|
||||
This requires `glslangValidator`. The SMAA sources retain their upstream license
|
||||
in `src/vr/smaa/`.
|
||||
|
||||
## Foveation tile calibration
|
||||
|
||||
Density-map texels and GMEM render tiles have different sizes. The gaze guard
|
||||
must protect every render tile touching the full-detail disk. The narrow entry
|
||||
in `include/mocktail/vr/foveation_tile_bounds.h` applies only to this identity:
|
||||
|
||||
- Turnip driver ID 18, vendor `0x5143`, device `0x43051401`, version `109060195`.
|
||||
- Driver name `turnip Mesa driver`, info `Mesa 26.3.0-devel (git-abc426bb62)`.
|
||||
- Driver UUID `586b9fa4514cb88b3c11a968ef4d62e4`; measured ELF build ID
|
||||
`b7e5995d0b202f06e81cd5a0f809ec1b744a0cb8`.
|
||||
- A 2160 × 2160 framebuffer, one layer/subpass, single-sample
|
||||
`R8G8B8A8_UNORM` color and `D32_SFLOAT` depth, plus the density attachment.
|
||||
Offsets, resolves, extra attachments, multiview and feedback/input attachments
|
||||
retain the conservative guard.
|
||||
|
||||
On that binary, `vkCmdBeginRenderPass2` at RVA `0xb12180` calls the layout
|
||||
selector at `0xad67c0`. The selector initializes its layout from the FDM flag
|
||||
at render-pass byte 145; subsequent checks can only select `AVOID_CCU` again.
|
||||
The divisor routine at `0xa9e300` returns 1 for FDM. CPU-only creation of 72
|
||||
render-pass/imageless-framebuffer combinations, covering FDM on/off and every
|
||||
standard color/depth load/store combination, produced identical budgets:
|
||||
356352 pixels for `FULL`, 350208 for `AVOID_CCU`. Their base tiles are
|
||||
480 × 736 and 768 × 448 respectively. FDM therefore selects 768 × 448
|
||||
independently of shaders and load/store operations for this tuple.
|
||||
|
||||
The GPU qualification compared a high-frequency full-density reference against
|
||||
the production map helper with its 68 × 68 map and 768 × 448 guard. Sweep both
|
||||
gaze axes through `{-1, -.75, -.4, 0, .4, .75, 1}`, radii
|
||||
`{.08, .15, .22, .35}`, and all three active profiles. All 588 cases preserved
|
||||
every pixel inside the gaze disk exactly, with 588 valid fragment-invocation
|
||||
queries and no validation errors. Mean fragment invocations were 51.46% of the
|
||||
reference in this fixture; this measures shading work, not game FPS. Requalify
|
||||
both the driver invariant and this image/counter sweep before adding identities
|
||||
or pass tuples. Unknown identities retain the conservative policy.
|
||||
|
||||
The CPU identity and coordinate contracts can be rerun with:
|
||||
|
||||
```sh
|
||||
cmake --build build-frame-arm64 --target foveation_tile_bounds_test framebuffer_density_map_test
|
||||
ctest --test-dir build-frame-arm64 -R 'FoveationTileBounds|FramebufferDensityMap' --output-on-failure
|
||||
```
|
||||
|
||||
## Frame-time stability
|
||||
|
||||
The Android `mallinfo` bridge shares a real host allocator snapshot for up to
|
||||
500 ms. glibc scans allocator free lists under arena locks; several consecutive
|
||||
queries therefore reuse a recent coherent sample. A scan runs outside the cache
|
||||
mutex, and concurrent readers can keep using the previous completed sample.
|
||||
Allocation, deallocation and the process-memory limit checks retain their normal
|
||||
behavior.
|
||||
|
||||
Gaze density generation reuses per-eye pixel and half-float buffers across
|
||||
frames. The density values and gaze protection are unchanged. At a 1:1 render and output size, the native path avoids unnecessary
|
||||
reconstruction, temporal resource tracking and upscaler shader dispatches.
|
||||
Gaze anti-aliasing runs on the composed eye image.
|
||||
|
||||
|
||||
The Vulkan eye copy is submitted on the OpenXR binding queue before releasing
|
||||
its swapchain images, so SteamVR can enqueue composition immediately. When the
|
||||
guest device requested exactly one unprotected queue, completion is deferred
|
||||
until the next use of its eye-copy and anti-aliasing resources. GPU queue ordering keeps subsequent guest
|
||||
commands behind the copy. The upstream submission ring tracks command buffers, fences and source/destination
|
||||
images. Presentation dependencies are consumed on the GPU and replaced with a
|
||||
per-swapchain-image semaphore, so the CPU does not wait for the current render.
|
||||
AA resources wait only for previous eye-copy users;
|
||||
current-frame present dependencies are excluded from that resource wait.
|
||||
Captured source-image handles are drained before
|
||||
image destruction; queue exposure, swapchain replacement and teardown also
|
||||
drain pending work before releasing resources. `xrWaitFrame` remains on the
|
||||
original render thread. Additional queues, readbacks and the virtual keyboard
|
||||
retain completed-copy synchronization. `MOCKTAIL_VR_EARLY_SUBMIT=0` restores
|
||||
the original submission order, while `MOCKTAIL_VR_DEFER_COPY_WAIT=0` keeps the
|
||||
early submission but completes its fence before waiting for the next frame.
|
||||
|
||||
Density-map generation skips the distance/ramp calculation inside the protected
|
||||
full-detail disk and beyond the peripheral plateau. Transition cells retain the
|
||||
same distance calculation and spatial pattern. This reduces CPU work without
|
||||
changing gaze protection or density values.
|
||||
|
||||
The Frame launcher enables a render-thread CPU preference after projection
|
||||
submission starts. It reads Linux CPU capacity values only for the thread's
|
||||
existing allowed CPUs and selects capacity peers within 80% of their maximum.
|
||||
Incomplete capacity information or fewer than two preferred CPUs retains the
|
||||
original allocation. This keeps render work on the application's performance
|
||||
cores while respecting SteamVR's CPU reservation. Other engine threads retain
|
||||
their normal allocation.
|
||||
|
||||
## Upstream integration
|
||||
|
||||
The imported Nightcap VR revision is recorded in [the credits](../THIRD_PARTY.md).
|
||||
The Frame launcher disables the generic full-eye FXAA pass so Steam Video's
|
||||
Anti Aliasing setting remains the single AA control. The upstream JNI haptic
|
||||
protocol runs natively on ARM64. Its optional x86 SetMotor instruction scanner
|
||||
is gated to x86; ARM64 build IDs, instruction checks and native return-value
|
||||
bridges retain their own contracts.
|
||||
|
||||
Steam Video settings are loaded once at startup and then refreshed on a worker.
|
||||
The rendering callbacks copy a completed snapshot; they never read the settings
|
||||
file or parse JSON. Unchanged content reuses its parsed values. A failed refresh
|
||||
keeps the last completed snapshot, and shutdown wakes the polling worker.
|
||||
The launcher gives a configured per-app headset refresh rate precedence over
|
||||
the desktop rate observed before the app gains focus, and derives Roblox's
|
||||
startup FPS cap from that selected profile.
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# Install on Steam Frame
|
||||
|
||||
## Requirements
|
||||
|
||||
- Steam Frame running its ARM64 Linux desktop, with Steam and SteamVR installed.
|
||||
- Native SteamVR at `/opt/steamvr`, including `steamxr_linuxarm64.json`.
|
||||
- The Frame's Turnip/Freedreno Vulkan driver under `/usr/lib` and its Vulkan ICD
|
||||
under `/usr/share/vulkan/icd.d`.
|
||||
- Rootless Podman, Git, Python 3 and a C++17 compiler on the Frame.
|
||||
- `zstd` for importing the compressed ARM64 release archive.
|
||||
- A Linux filesystem for the checkout, internet access and space for the source,
|
||||
Fedora build image and a separate Roblox client download.
|
||||
|
||||
The runtime uses Fedora 44 ARM64 in a rootless container. Keep the Frame's
|
||||
own graphics drivers: the launcher mounts them into the container. Building
|
||||
from source also needs a case-sensitive Linux checkout because some Android
|
||||
headers differ only by letter case. The source and ARM64 build are public.
|
||||
|
||||
## 1. Get the source
|
||||
|
||||
```bash
|
||||
git clone --branch steam-frame --recurse-submodules \
|
||||
https://github.com/InstanceBrick/nightcap.git \
|
||||
"$HOME/.local/share/nightcap-steam-frame-src"
|
||||
cd "$HOME/.local/share/nightcap-steam-frame-src"
|
||||
```
|
||||
|
||||
## 2. Install the ARM64 image
|
||||
|
||||
Download `nightcap-steam-frame-arm64.oci.tar.zst` and its `.sha256` file from
|
||||
[Releases](https://github.com/InstanceBrick/nightcap/releases). From the
|
||||
directory containing both files:
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
sha256sum -c nightcap-steam-frame-arm64.oci.tar.zst.sha256
|
||||
zstd -dc nightcap-steam-frame-arm64.oci.tar.zst | podman load
|
||||
```
|
||||
|
||||
The archive imports as `localhost/nightcap-vr-arm64:frame-foveated`. It contains
|
||||
the ARM64 Nightcap runtime and its Fedora dependencies, with no Roblox client,
|
||||
personal data, SteamVR installation or proprietary graphics drivers.
|
||||
|
||||
To build the same image from source instead:
|
||||
|
||||
```bash
|
||||
podman build --arch arm64 --jobs 1 \
|
||||
-f packaging/steam-frame/Containerfile \
|
||||
-t localhost/nightcap-vr-arm64:frame-foveated .
|
||||
```
|
||||
|
||||
This builds Nightcap and its helpers from source. It does not download or bundle
|
||||
Roblox. The image tag is also the one used by the Frame launcher.
|
||||
|
||||
## 3. Install the launcher and refresh-rate helper
|
||||
|
||||
```bash
|
||||
python3 tools/steam_frame/install_launcher.py
|
||||
mkdir -p "$HOME/.local/share/nightcap-vr-arm64/data/tools"
|
||||
c++ -std=c++17 -O2 tools/frame_video_settings/refresh_rate.cc -ldl \
|
||||
-o "$HOME/.local/share/nightcap-vr-arm64/data/tools/refresh_rate"
|
||||
```
|
||||
|
||||
The application menu entry is **Nightcap Roblox VR**.
|
||||
|
||||
## 4. Download the compatible Roblox client
|
||||
|
||||
Close Nightcap first. Prepare its data directory and copy the initial settings
|
||||
only if no configuration exists yet:
|
||||
|
||||
```bash
|
||||
frame_data="$HOME/.local/share/nightcap-vr-arm64/data"
|
||||
mkdir -p "$frame_data/.config/mocktail"
|
||||
if [ ! -f "$frame_data/.config/mocktail/config.yaml" ]; then
|
||||
cp config/steam-frame.example.yaml "$frame_data/.config/mocktail/config.yaml"
|
||||
fi
|
||||
podman run --rm --arch arm64 --userns=keep-id \
|
||||
--security-opt label=disable \
|
||||
-v "$frame_data:/data:rw" \
|
||||
-e HOME=/data -e XDG_CONFIG_HOME=/data/.config \
|
||||
-e XDG_DATA_HOME=/data/.local/share -e XDG_CACHE_HOME=/data/.cache \
|
||||
--entrypoint /opt/nightcap-vr-arm64/lib64/mocktail/mocktail_updater \
|
||||
localhost/nightcap-vr-arm64:frame-foveated \
|
||||
update --no-latest-check --skip-canary
|
||||
```
|
||||
|
||||
The updater downloads the newest ARM64 client supported by the included catalog.
|
||||
The command retains APK signature, architecture, build identity and payload
|
||||
integrity checks. `--skip-canary` skips the separate desktop graphics test; VR
|
||||
starts through the Frame launcher in the next step. Client files stay in your
|
||||
local data directory.
|
||||
|
||||
## 5. Launch in VR and add to Steam
|
||||
|
||||
Start SteamVR and connect the headset, then open **Nightcap Roblox VR** from the
|
||||
application menu. Sign in through Roblox's normal interface.
|
||||
|
||||
To add the Steam shortcut automatically, enable Steam's local browser debugging
|
||||
endpoint at `127.0.0.1:8080`, keep Steam running and use:
|
||||
|
||||
```bash
|
||||
python3 tools/steam_frame/register_steam_shortcut.py
|
||||
```
|
||||
|
||||
The script creates or reuses the launcher shortcut, sets its Nightcap icon and
|
||||
marks it as a VR application. It saves the generated identity locally. If the
|
||||
endpoint is unavailable, use Steam's **Add a Non-Steam Game** dialog and select
|
||||
`~/.local/bin/nightcap-vr-arm64`, then mark the shortcut as a VR application.
|
||||
|
||||
## 6. Add the SteamVR Video controls
|
||||
|
||||
The settings integration supports a specific SteamVR dashboard revision. It
|
||||
checks the installed dashboard before applying changes and leaves other
|
||||
revisions untouched. Valve dashboard files are read from your local installation.
|
||||
|
||||
With SteamVR closed and the original dashboard files visible:
|
||||
|
||||
```bash
|
||||
mkdir -p frame-ui-local/originals frame-ui-local/generated
|
||||
cp /opt/steamvr/resources/webinterface/dashboard/chunk~8012d0c89.js \
|
||||
frame-ui-local/originals/steamvr-dashboard.original.js
|
||||
cp /opt/steamvr/resources/webinterface/dashboard/systemui.html \
|
||||
frame-ui-local/originals/steamvr-systemui.original.html
|
||||
cp /opt/steamvr/resources/webinterface/dashboard/settings_desktop.html \
|
||||
frame-ui-local/originals/steamvr-settings_desktop.original.html
|
||||
python3 tools/steam_frame/prepare_video_ui.py \
|
||||
frame-ui-local/originals frame-ui-local/generated
|
||||
sudo python3 tools/steam_frame/install_video_ui.py frame-ui-local/generated
|
||||
```
|
||||
|
||||
Start SteamVR again. The settings are described in [Video settings](SETTINGS.md).
|
||||
The service mounts three generated files read-only; the original SteamVR files
|
||||
are preserved. If upgrading an existing installation, reuse your saved original
|
||||
files instead of copying the already patched dashboard.
|
||||
|
||||
## Update Nightcap
|
||||
|
||||
Close Nightcap before replacing its image or launcher. Pull the `steam-frame`
|
||||
branch, then load the new ARM64 release archive as in step 2 and rerun the
|
||||
launcher installer. Existing Roblox downloads, sign-in state and settings stay
|
||||
in the separate data directory.
|
||||
|
||||
For source builds, pull this branch and rebuild under a temporary image tag:
|
||||
|
||||
```bash
|
||||
git pull --ff-only
|
||||
git submodule update --init --recursive
|
||||
podman build --arch arm64 --jobs 1 \
|
||||
-f packaging/steam-frame/Containerfile \
|
||||
-t localhost/nightcap-vr-arm64:next .
|
||||
podman tag localhost/nightcap-vr-arm64:frame-foveated \
|
||||
localhost/nightcap-vr-arm64:previous
|
||||
podman tag localhost/nightcap-vr-arm64:next \
|
||||
localhost/nightcap-vr-arm64:frame-foveated
|
||||
python3 tools/steam_frame/install_launcher.py
|
||||
```
|
||||
|
||||
Existing Roblox downloads, sign-in state and settings remain in the data
|
||||
directory. To install a newly supported Roblox client, repeat step 4 after
|
||||
updating Nightcap. Automatic client updates are off in the supplied Frame
|
||||
configuration, since the ARM64 VR bridge needs matching client interfaces.
|
||||
|
||||
To restore the previous Nightcap image with the app closed:
|
||||
|
||||
```bash
|
||||
podman tag localhost/nightcap-vr-arm64:previous \
|
||||
localhost/nightcap-vr-arm64:frame-foveated
|
||||
```
|
||||
@@ -1,258 +0,0 @@
|
||||
# Performance and CPU load
|
||||
|
||||
What Nightcap does to your CPU and GPU, which knobs change it, and how to
|
||||
measure rather than guess.
|
||||
|
||||
## GameMode core pinning
|
||||
|
||||
Nightcap asks Feral GameMode for a performance session at startup
|
||||
(`performance.gamemode`, default `auto`). GameMode raises the CPU governor and
|
||||
the process priority, which helps.
|
||||
|
||||
On a CPU with a wide spread of per-core maximum frequencies — Intel P-core and
|
||||
E-core parts, AMD parts with preferred cores — GameMode also pins the process
|
||||
to the fastest cores. On a 32-thread host that can mean 4 usable threads out of
|
||||
32. Roblox spreads its work across a worker pool, so the pin concentrates that
|
||||
pool onto a couple of physical cores instead of spreading it. Those cores sit
|
||||
near their boost ceiling and run hot while the rest of the chip idles.
|
||||
|
||||
GameMode re-applies the pin roughly every 5 seconds, so widening the mask from
|
||||
inside the process does not hold. Releasing the request does release the pin.
|
||||
|
||||
Under `auto`, Nightcap therefore checks its CPU affinity immediately after the
|
||||
request. If GameMode narrowed it, Nightcap ends the request and prints:
|
||||
|
||||
```
|
||||
[gamemode] released: it pinned this process to a subset of the CPUs, ...
|
||||
```
|
||||
|
||||
The trade is that GameMode's governor and priority boost go with it. To keep
|
||||
those and lose only the pinning, turn pinning off for every game on the host.
|
||||
|
||||
These are the settings to run with:
|
||||
|
||||
```ini
|
||||
# ~/.config/gamemode.ini
|
||||
[general]
|
||||
desiredgov=performance
|
||||
|
||||
[cpu]
|
||||
pin_cores=no
|
||||
park_cores=no
|
||||
```
|
||||
|
||||
`desiredgov` is the one to check first. The packaged default in
|
||||
`/usr/share/gamemode/gamemode.ini` is already `performance`, but a value in
|
||||
your own file replaces it, and `desiredgov=powersave` there means GameMode
|
||||
clocks the CPU **down** for the whole session while still reporting
|
||||
`[gamemode] performance request active`. The symptoms are a low CPU clock
|
||||
under load, a GPU that never leaves its lower SM clocks, and the engine pump
|
||||
falling back to `tpause` — which costs about 70% of a core instead of 3%,
|
||||
because `sleep` is only chosen when the governor reads `performance`. On one
|
||||
13900K host, correcting `powersave` to `performance` took the process from
|
||||
177% CPU to 42% and the package from 70°C to 53-63°C, at the same frame rate.
|
||||
|
||||
GameMode reads `$XDG_CONFIG_HOME`, `/usr/share/gamemode` and `/etc`, in that
|
||||
order. It does not read anything under Nightcap's own config directory, so this
|
||||
file has to live in one of those three places. The daemon reads it when it
|
||||
starts, so restart it after editing: `systemctl --user restart gamemoded`.
|
||||
|
||||
Restarting the daemon drops the session of any game already running; relaunch
|
||||
the game for new settings to apply to it.
|
||||
|
||||
`gamemoded` is D-Bus activated through
|
||||
`com.feralinteractive.GameMode.service`, so `systemctl --user stop gamemoded`
|
||||
does not hold — the next client request starts it again within seconds. To
|
||||
measure with GameMode genuinely off, mask it:
|
||||
|
||||
```bash
|
||||
systemctl --user mask gamemoded # and --unmask afterwards
|
||||
```
|
||||
|
||||
The governor switch runs through polkit. Debian and Ubuntu only allow it for
|
||||
members of the `gamemode` group; without that, the daemon's journal shows
|
||||
`cpugovctl set performance: Not authorized` and the governor stays put while
|
||||
the session is otherwise active. Add yourself and log in again:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG gamemode "$USER"
|
||||
```
|
||||
|
||||
Fedora and Arch allow any logged-in local user.
|
||||
|
||||
Setting `performance.gamemode: on` accepts the pinning instead: `on` is an
|
||||
explicit request for GameMode, and Nightcap keeps the session whatever it does
|
||||
to the affinity. `off` never contacts the daemon.
|
||||
|
||||
To see which CPUs the process may use:
|
||||
|
||||
```bash
|
||||
pid=$(pgrep -x Main)
|
||||
for t in /proc/$pid/task/*; do taskset -pc "${t##*/}"; done
|
||||
```
|
||||
|
||||
Two traps here. The engine process reports its name as `Main`, not
|
||||
`mocktail`; `pgrep -x mocktail` finds only the launcher, which sits near 0%
|
||||
CPU and tells you nothing. And `taskset -p` reads one thread, not the whole
|
||||
process, so a single call reports the main thread's mask and says nothing
|
||||
about the workers — which is where the game's load actually is.
|
||||
|
||||
For the same reason, `ps -L -o psr=` shows which core each thread is running
|
||||
on right now, not which cores it is allowed on. On a hybrid CPU the scheduler
|
||||
preferring P-cores for runnable work is normal and is not a mask.
|
||||
|
||||
## The main-thread pump
|
||||
|
||||
Roblox posts its per-frame main-thread step at a time of its own choosing,
|
||||
and nothing signals the host when it does. The host main thread therefore
|
||||
polls `nativeCallMessagesFromMainThread`, and a post that waits more than a
|
||||
fraction of a millisecond costs a whole frame. Between polls the thread rests
|
||||
for 100 µs, and how it rests decides the CPU clock:
|
||||
|
||||
| Mode | When | What it costs |
|
||||
| --- | --- | --- |
|
||||
| `sleep` | The governor is `performance`, which GameMode sets | The thread idles at about 3% of a core. The governor holds the clock. |
|
||||
| `tpause` | Otherwise, on a CPU with WAITPKG (Intel 12th gen and newer) | The core stays awake in a light C0 state. Frame rate holds; the OS books the time as busy, so the thread shows around 70% of a core, at lower power than a spin. |
|
||||
| `spin` | Otherwise | The core stays awake at full power. |
|
||||
|
||||
Under a power-saving governor the CPU clocks a bursty core down unless some
|
||||
core stays awake, and Roblox's render thread then runs slower and drops
|
||||
frames. That is why `sleep` is not the default there. Startup prints the
|
||||
chosen mode:
|
||||
|
||||
```
|
||||
[main] engine pump rest: tpause (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)
|
||||
```
|
||||
|
||||
`MOCKTAIL_ENGINE_PUMP_REST` forces a mode. `sleep` without the governor's help
|
||||
runs cooler for a few frames per second less; measure before keeping it.
|
||||
|
||||
## Small texture upscaling
|
||||
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) stores ETC2 textures of 64px or
|
||||
smaller at N times their width and height. At the default that is 16 times the
|
||||
pixels per affected texture.
|
||||
|
||||
Upscaling alone adds no detail. Each original pixel becomes an N×N block of the
|
||||
same colour, and the GPU's filtering already blends a small texture that much
|
||||
when it draws it. The extra size only shows when a replacement PNG in
|
||||
`~/.config/mocktail/textures` has more detail than the original.
|
||||
|
||||
The cost lands in two places. Decoding and resampling is CPU work on up to 8
|
||||
threads while textures load, which shows up as a burst rather than a steady
|
||||
load. The enlarged textures then stay in GPU memory for the session and cost
|
||||
extra sampling bandwidth on every frame that uses them. In Blade Ball the
|
||||
memory share is small: decoded textures came to 5.1 times their ETC2 size both
|
||||
with the default and with upscaling off. The `emulated texture (MB)` and
|
||||
`compressed texture (MB)` counters in a `--profile` trace give the figures for
|
||||
any game.
|
||||
|
||||
That decode does not run on the thread that submits the frame. On a device
|
||||
with a timeline semaphore, uploads decode on a background dispatcher and the
|
||||
submit carries a semaphore wait, so the GPU waits for the texture instead of
|
||||
the render thread. In the trace this is the `etc2 decode` slice on its own
|
||||
thread, with only a negligible `etc2 gather` left on the submitting one. A
|
||||
device without that feature decodes inline as before.
|
||||
|
||||
Set it to `1` to turn upscaling off:
|
||||
|
||||
```bash
|
||||
MOCKTAIL_SMALL_TEXTURE_UPSCALE=1 mocktail
|
||||
```
|
||||
|
||||
## Native code in the menus
|
||||
|
||||
The home screen and avatar editor run Roblox's Lua app in the Luau interpreter,
|
||||
and its long frames come from that thread. PC profiles set
|
||||
`FFlagDebugLuauCodegenAll`, which compiles every script to native code. That
|
||||
makes the menus' Lua about 2.5 times cheaper for about 56 MB of compiled code.
|
||||
|
||||
In a game the same flag compiles every game script too: Blade Ball grew from 7
|
||||
MB to about 300 MB and hit Roblox's native code limit, which shows warnings in
|
||||
F9. So Nightcap turns the flag off when a game starts joining and back on when
|
||||
you return to the menus. Roblox reads the flag each time it compiles a script,
|
||||
and a game's compiled code is freed when you leave it. Games run with Roblox's
|
||||
own per-script choice, the same as without the flag.
|
||||
|
||||
The log shows each switch:
|
||||
|
||||
```text
|
||||
[luau-codegen] menus-only switch at +0x<offset in libroblox.so>
|
||||
[luau-codegen] native code for all scripts off
|
||||
[luau-codegen] native code for all scripts on
|
||||
```
|
||||
|
||||
Setting `FFlagDebugLuauCodegenAll` in `fflags.json` turns the switching off
|
||||
and your value applies everywhere. To keep the interpreter in the menus too:
|
||||
|
||||
```json
|
||||
{
|
||||
"FFlagDebugLuauCodegenAll": false
|
||||
}
|
||||
```
|
||||
|
||||
## Frame rate and present mode
|
||||
|
||||
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
|
||||
framerate cap in charge; Nightcap sets no `DFIntTaskSchedulerTargetFps`
|
||||
override. A fixed value forwards that number to the scheduler.
|
||||
|
||||
In VR, `vr.frame_rate_limit` (`MOCKTAIL_VR_FRAME_RATE_LIMIT`) replaces
|
||||
`graphics.frame_rate_limit` and takes the same values. Its default is
|
||||
`unlimited`: the scheduler target goes to 240 and `xrWaitFrame` holds each
|
||||
frame to the headset's refresh rate. Set `-1` to restore Roblox's own cap, or a
|
||||
number such as `90` to pin it.
|
||||
|
||||
`MOCKTAIL_VR_RENDER_SCALE` (default `1.0`, range `0.25`–`2.0`) scales each
|
||||
axis of the OpenXR runtime's recommended eye size. SteamVR's recommendation
|
||||
already includes its own supersampling, so on a Quest 2 at 90 Hz it is about
|
||||
2244x2352 per eye. If a frame misses the 11.1 ms budget, the runtime halves
|
||||
the rate to 45 FPS; `0.75` renders about 56% of the pixels.
|
||||
|
||||
`MOCKTAIL_VR_UNPACED=1` is a benchmark mode. Roblox no longer waits for the
|
||||
headset between frames; each XR frame stays open across several Roblox frames
|
||||
and is submitted with the newest eyes once per display period. Pair it with a
|
||||
fixed scheduler target above 240 and `MOCKTAIL_VSYNC=off` to find the
|
||||
maximum rate:
|
||||
|
||||
```bash
|
||||
MOCKTAIL_VR_UNPACED=1 MOCKTAIL_VR_FRAME_RATE_LIMIT=9999 MOCKTAIL_VSYNC=off mocktail --vr
|
||||
```
|
||||
|
||||
Frames beyond the headset refresh are never displayed, and they all reuse the
|
||||
open frame's head pose, so expect judder. Do not play in this mode.
|
||||
|
||||
`graphics.vsync` (default `auto`) selects the presentation mode. `auto` and
|
||||
`on` both pick the lowest-latency synchronized mode the driver offers, in
|
||||
order: `FIFO_LATEST_READY`, `MAILBOX`, `FIFO_RELAXED`, `FIFO`. The first two do
|
||||
not block the render thread on the display refresh, so the frame rate is
|
||||
governed by the in-game cap rather than by presentation. `off` selects
|
||||
`IMMEDIATE` and does not synchronize at all.
|
||||
|
||||
If the frame rate is higher than your refresh rate, the extra frames are work
|
||||
you cannot see. Lower the in-game cap to your refresh rate first.
|
||||
|
||||
## Measuring
|
||||
|
||||
Shift+F4 in game shows Roblox's own scheduler jobs, with a millisecond cost and
|
||||
a percentage for each. It does not show Nightcap's threads — the Vulkan
|
||||
adapter, the ETC2 decode workers, the libc shim — so it is a reading on the
|
||||
game, not on the process.
|
||||
|
||||
For the whole process:
|
||||
|
||||
```bash
|
||||
top -H -p $(pgrep -x mocktail) # per-thread CPU, sorted
|
||||
ps -L -o psr=,pcpu=,comm= -p $(pgrep -x mocktail) # which core each thread is on
|
||||
nvidia-smi --query-gpu=utilization.gpu,temperature.gpu,power.draw --format=csv
|
||||
```
|
||||
|
||||
A thread pinned near 100% while the frame rate is steady is a busy-wait. The
|
||||
one exception is the host main thread in `tpause` mode, which the OS reports
|
||||
as busy while the core is parked (see above). Load spread across many
|
||||
`RBX Worker` threads is the game's own scheduler, and its cost tracks the
|
||||
place you are in — part count, moving parts and joints all show in the
|
||||
Shift+F4 World section.
|
||||
|
||||
`--profile <file>` writes a Chrome trace of the Vulkan adapter's work for
|
||||
ui.perfetto.dev. See [BENCHMARKING.md](BENCHMARKING.md).
|
||||
@@ -1,67 +0,0 @@
|
||||
# Nightcap roadmap
|
||||
|
||||
What Nightcap is working toward, in priority order. Items move to
|
||||
[Shipped](#shipped) when they land on `main`. Open an issue to propose a
|
||||
change to this list.
|
||||
|
||||
## 1. Settings UI
|
||||
|
||||
Nightcap is configured by editing `~/.config/mocktail/config.yaml`. Most
|
||||
people never open it, so they never find the FPS unlock or the vsync switch.
|
||||
|
||||
Goal: a small GTK4 settings window, reachable from the app menu and from a
|
||||
`--settings` flag, that reads and writes the same `config.yaml`.
|
||||
|
||||
- Graphics tab: backend, frame rate limit, vsync, small texture upscale.
|
||||
- Input tab: the `input.raw_mouse` toggle and controller options.
|
||||
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
|
||||
- Discord tab: every field in the `discord_rpc` block, so a friend can see
|
||||
what you are playing and you can switch it all off in one click.
|
||||
- Changes apply on next launch. A restart button restarts Nightcap.
|
||||
|
||||
Done when every option in `config/mocktail.example.yaml` that a player would
|
||||
touch can be set without a text editor.
|
||||
|
||||
## Shipped
|
||||
|
||||
- Lower input latency: frames are presented as soon as they are ready.
|
||||
- Controllers hot-plug without a restart, and a pad that is unplugged mid-game
|
||||
releases its held buttons and sticks instead of leaving them stuck.
|
||||
- Mouse look keeps responding after a long turn, instead of going dead until
|
||||
the pointer travels back from past the screen edge.
|
||||
- Optional raw mouse input: `input.raw_mouse` in `config.yaml` sends look
|
||||
deltas in host units, so fractional display scaling no longer changes
|
||||
first-person sensitivity.
|
||||
- Small textures upscaled 4x by default, with PNG overrides in
|
||||
`~/.config/mocktail/textures`.
|
||||
- ETC2 textures decoded on desktop GPUs without stutter.
|
||||
- No more error 319 kicks after joining a server.
|
||||
- Roblox sees the real host RAM and screen size.
|
||||
- Desktop launcher that works on Wayland and finds the installed binary.
|
||||
- Discord Rich Presence on by default, with the experience name, creator,
|
||||
elapsed time, the experience icon with a Roblox badge, and a join button
|
||||
when the server is public. See the [FAQ](../FAQ.md) to switch it off.
|
||||
- Custom Discord presence: title, details, state, and large and small icons
|
||||
from `config.yaml`, with `{place_name}`, `{place_icon}`, and
|
||||
`{creator_name}` placeholders.
|
||||
- `--profile` writes a Chrome-format trace of the Vulkan adapter's work,
|
||||
including shader and pipeline creation, readable in `ui.perfetto.dev`.
|
||||
[BENCHMARKING.md](BENCHMARKING.md) fixes the experiences to measure and
|
||||
`scripts/summarize_profile_trace.py` builds the before and after table.
|
||||
- Texture uploads no longer stall the frame they land on: ETC2 decode and
|
||||
resampling run on a persistent worker pool.
|
||||
- Less stutter while joining an experience and while textures load in. See
|
||||
the [changelog](../CHANGELOG.md) for the numbers.
|
||||
- The host main thread rests between engine polls instead of spinning, and
|
||||
GameMode's core pinning is released, so Roblox's workers spread across
|
||||
every core. See [PERFORMANCE.md](PERFORMANCE.md).
|
||||
- Own app ID `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream.
|
||||
- AppImage and signed Flatpak releases on every `v*` tag, with the Flatpak
|
||||
repo on GitHub Pages.
|
||||
|
||||
## Not planned
|
||||
|
||||
- Replacing the `mocktail` binary and config names. Keeping them makes
|
||||
upstream merges clean.
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Settings
|
||||
|
||||
## Video
|
||||
|
||||
Open the game's SteamVR **Video** settings after installing the settings
|
||||
integration.
|
||||
|
||||
The standard profile is **native 2160 × 2160 per eye**, **72 Hz**, **SMAA High**
|
||||
and **Performance** foveated rendering. The application targets 72 FPS at 72 Hz;
|
||||
actual frame rate depends on the experience and Roblox's graphics settings.
|
||||
The gaze radius remains adjustable, and existing saved choices are preserved.
|
||||
|
||||
| Setting | Effect |
|
||||
| --- | --- |
|
||||
| Foveated Rendering | Off uses full-density shading throughout. Quality, Balanced and Performance progressively reduce peripheral shading work. Each mode keeps the same full-detail gaze region. |
|
||||
| Gaze sharp radius | Sets the minimum full-detail region around your gaze, from 8% to 35%. Larger values preserve more of the view at full density and cost more GPU time. Hardware tile boundaries can enlarge this region. |
|
||||
| Anti Aliasing | Off, FXAA, SMAA Low or SMAA High. Filtering runs around your gaze on the composed eye image, including interface edges. SMAA High is the default. |
|
||||
| Desktop Mirror | Off by default on Frame. On displays the VR image in the desktop window as well as the headset. Relaunch Nightcap after changing it. |
|
||||
| Performance Overlay | Shows actual game FPS, headset Hz, average frame time, SteamVR GPU time and per-eye resolution at the bottom center of the headset view. It hides while the Steam menu is open and does not capture controller input. |
|
||||
| Resolution Per Eye | Sets the headset eye-buffer size. Nightcap's recommended default is 2160 × 2160 per eye; Auto and Reset to Defaults use this baseline. Explicit custom values stay selected. Changes apply after the slider settles, with a brief pause while eye buffers are rebuilt at a safe frame boundary. |
|
||||
| Display Refresh Rate | The standard profile uses 72 Hz. The launcher follows the selected per-game headset rate and any explicit Steam frame limit. Restart Nightcap after changing the rate or frame limit so Roblox's internal cap matches it. |
|
||||
|
||||
Enable **Performance Overlay**, then close the Steam menu. **APP FPS** counts
|
||||
new Roblox projections over a rolling second. **HMD Hz** is the headset refresh
|
||||
rate. **FRAME** is average time per submitted game frame, **TARGET** is the
|
||||
configured application frame rate, and **BUDGET** is the time available per
|
||||
application frame: 13.89 ms for 72 FPS at 72 Hz. Repeated or reprojected
|
||||
compositor frames do not count as new game frames.
|
||||
|
||||
**GPU** averages SteamVR timings for new application frames, excluding
|
||||
compositor-only reprojections. It can include scheduling gaps. The overlay
|
||||
reports standby or missing frames instead of retaining a stale FPS. Its setting
|
||||
persists between launches and can be turned off at any time.
|
||||
|
||||
Foveated rendering reduces fragment-shader work in the 3D scene before the
|
||||
image is composed. The final composition stays at native resolution, keeping
|
||||
screen-space text and interface elements from being coarsened a second time.
|
||||
Your gaze region retains the detail available at the selected eye resolution;
|
||||
the periphery uses progressively lower shading density.
|
||||
|
||||
Foveation, gaze radius and anti-aliasing controls update while the application
|
||||
is running. Higher eye resolutions improve fine detail and increase GPU and
|
||||
memory cost. For more headroom, keep Performance foveation, reduce the gaze
|
||||
radius if needed, or lower Roblox's own graphics quality.
|
||||
|
||||
The launcher uses the selected Steam Video resolution without an extra
|
||||
multiplier. To add supersampling, set `NIGHTCAP_VR_RENDER_SCALE` when launching
|
||||
Nightcap; for example, `NIGHTCAP_VR_RENDER_SCALE=1.25`. The default is `1.0`.
|
||||
Larger values increase GPU work.
|
||||
|
||||
The launcher reads the selected headset refresh rate and SteamVR's frame limit
|
||||
on every launch. At 72 Hz with no frame limit, Roblox's internal scheduler is
|
||||
capped at 72 FPS. Half-rate rendering is not forced automatically.
|
||||
|
||||
On Frame, the render thread prefers higher-capacity cores within the CPU
|
||||
allocation supplied by SteamVR. To use the default thread placement, launch with
|
||||
`NIGHTCAP_VR_PREFER_PERFORMANCE_CORES=0`.
|
||||
|
||||
## Controllers and typing
|
||||
|
||||
The native VR bridge supplies controller tracking, buttons, axes and haptics.
|
||||
Roblox experiences choose how to use those inputs. The in-headset keyboard opens
|
||||
when a text field receives focus; point at a key and pull the trigger to type.
|
||||
Steam Controller desktop navigation can also be configured through the Steam
|
||||
shortcut's controller layout.
|
||||
|
||||
## Microphone
|
||||
|
||||
Set the Frame's headset microphone as the default input in the desktop audio
|
||||
settings and check that it is not muted. Roblox voice chat uses its normal
|
||||
account eligibility, microphone permission and in-experience controls.
|
||||
|
||||
Nightcap's native recording bridge captures the selected input when Roblox
|
||||
requests recording. To choose a different input, close Nightcap and edit
|
||||
`~/.local/share/nightcap-vr-arm64/data/.config/mocktail/config.yaml`:
|
||||
|
||||
```yaml
|
||||
audio:
|
||||
input_device: default
|
||||
```
|
||||
|
||||
An exact SDL device name can replace `default`. Input device names appear in the
|
||||
launch log.
|
||||
|
||||
## Desktop mirror
|
||||
|
||||
Open SteamVR's per-game **Video** settings and set **Desktop Mirror** to **On**
|
||||
to show the VR view in a desktop window. Set it to **Off** to keep the Frame's
|
||||
single headset application without the extra desktop view. Relaunch Nightcap
|
||||
after changing this setting. The Frame default is **Off**.
|
||||
|
||||
The Frame launcher reads the saved SteamVR mirror choice and coordinates both
|
||||
the mirror renderer and desktop-window visibility. This setting takes precedence
|
||||
over the YAML value when using the Frame launcher. Generic Nightcap launches
|
||||
can configure the mirror beside the VR option:
|
||||
|
||||
```yaml
|
||||
vr:
|
||||
enabled: true
|
||||
desktop_mirror: true
|
||||
```
|
||||
|
||||
`MOCKTAIL_VR_DESKTOP_MIRROR=1` or `0` overrides the YAML setting for generic
|
||||
launches. VR and desktop mirroring are separate options; enabling VR does not
|
||||
require the desktop mirror.
|
||||
|
||||
Roblox's desktop viewport stays at 1920 × 1080 during VR for stable in-game
|
||||
menu size, independently of headset eye resolution. On Frame, desktop window geometry
|
||||
is not restored or saved in VR, including when the mirror is enabled.
|
||||
|
||||
## Files and troubleshooting
|
||||
|
||||
| Item | Location |
|
||||
| --- | --- |
|
||||
| Launcher | `~/.local/bin/nightcap-vr-arm64` |
|
||||
| Launch log | `~/.local/share/nightcap-vr-arm64/last-launch.log` |
|
||||
| Configuration | `~/.local/share/nightcap-vr-arm64/data/.config/mocktail/config.yaml` |
|
||||
| Local client store | `~/.local/share/nightcap-vr-arm64/data/.local/share/mocktail` |
|
||||
| Steam shortcut identity | `~/.local/share/nightcap-vr-arm64/steam-shortcut.json` |
|
||||
|
||||
If launching fails, check the launch log, make sure SteamVR is running and confirm
|
||||
that the `localhost/nightcap-vr-arm64:frame-foveated` image exists. If the game
|
||||
reports an unsupported client, update Nightcap before replacing the client.
|
||||
Keep account data and client downloads outside source control.
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
# VR
|
||||
|
||||
Experimental OpenXR VR is opt-in: launch with `--vr`, or set `vr.mode` in
|
||||
`config.yaml`:
|
||||
|
||||
- `off` (default) always starts on the desktop.
|
||||
- `on` always starts in VR.
|
||||
- `auto` starts in VR when SteamVR is already running with a connected
|
||||
headset, ALVR included, and on the desktop otherwise. It never starts
|
||||
SteamVR, so start SteamVR and connect the headset first.
|
||||
|
||||
`--vr` and `--no-vr` override `vr.mode` for one launch. The older
|
||||
`vr.enabled: true`/`false` still works as `on`/`off`. Everything here only
|
||||
applies while VR is active. The [README](../README.md#experimental-vr) covers
|
||||
building and running.
|
||||
|
||||
## Menus and typing
|
||||
|
||||
The home menu uses Roblox's `More` button. The desktop side panel is turned
|
||||
off in VR, because the VR pointer cannot click it.
|
||||
|
||||
When a text box takes focus (chat, search), a keyboard opens in front of you,
|
||||
below eye level. It is a full US layout without the function row or
|
||||
Ctrl/Alt. Point at a key and pull the trigger. While you point at the keyboard,
|
||||
the trigger types and does not click the game behind it. Hold Backspace to
|
||||
delete repeatedly. Shift applies to the next key and selects its upper symbol;
|
||||
Caps Lock stays on until you tap it again. Close dismisses the keyboard and
|
||||
leaves the text box. Set `MOCKTAIL_VR_KEYBOARD=0` to turn the keyboard off.
|
||||
|
||||
## Environment variables
|
||||
|
||||
## Runtime and display
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `MOCKTAIL_VR_ENABLED` | `0` | Same as `--vr` (`1`) or `--no-vr` (`0`). |
|
||||
| `MOCKTAIL_VR_RUNTIME` | `auto` | OpenXR runtime: `auto`, `system`, `wivrn`, `steamvr` or `alvr`. `XR_RUNTIME_JSON` overrides it. |
|
||||
| `MOCKTAIL_VR_BACKEND` | `xr` | `native` keeps Roblox's stereo rendering without an OpenXR runtime, for debugging. |
|
||||
| `MOCKTAIL_VR_FRAME_RATE_LIMIT` | `unlimited` | Frame-rate cap in VR, same values as `graphics.frame_rate_limit` (also `vr.frame_rate_limit`). With `unlimited` the headset's refresh rate paces frames. |
|
||||
| `MOCKTAIL_VR_ANTI_ALIASING` | `fxaa` | Same as `vr.anti_aliasing`. Roblox draws the VR eyes without anti-aliasing, so each eye is filtered on its way to the headset (Vulkan only). `fxaa` is the cheapest; `smaa` finds edges more precisely and keeps text sharper; `off` sends them as rendered. |
|
||||
| `MOCKTAIL_VR_RENDER_SCALE` | `1.0` | Scales each axis of the runtime's recommended eye size, `0.25` to `2.0`. |
|
||||
| `MOCKTAIL_VR_UNPACED` | `0` | `1` stops waiting for the headset between frames, to measure the maximum frame rate. Frames past the headset refresh are not shown. Not for playing. |
|
||||
| `MOCKTAIL_VR_DESKTOP_MIRROR` | `1` | `0` stops copying the left eye to the desktop window. |
|
||||
| `MOCKTAIL_VR_MIRROR_SHM` | unset | A shared-memory name such as `/nightcap-mirror` publishes both eyes for an external viewer. The segment is private to your user. |
|
||||
| `MOCKTAIL_VR_KEYBOARD` | `1` | `0` turns off the in-headset keyboard. |
|
||||
|
||||
## Hands and scale
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `MOCKTAIL_VR_HAND_POSE_SOURCE` | `aim` | `grip` orients hands from the controller grip pose instead of the aim pose. |
|
||||
| `MOCKTAIL_VR_HAND_PITCH_OFFSET` | `-20` | Degrees added to Roblox's own hand pitch. |
|
||||
| `MOCKTAIL_VR_HAND_PITCH` | unset | Replaces the total hand pitch, in degrees. |
|
||||
| `MOCKTAIL_VR_WORLD_SCALE` | `1.0` | Scales head, hand and eye offsets. `MOCKTAIL_VR_EYE_OFFSET_SCALE` is an older name for it. |
|
||||
|
||||
## Performance tracing
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `MOCKTAIL_VR_PERF` | unset | `1` logs per-stage frame timings and counters once a second. `present_interval` is the frame time; `desktop_mirror`, `copy_record` and `copy_fence_wait` show time the CPU spent on VR copies. |
|
||||
| `MOCKTAIL_VR_MEASURE_START_NS`, `MOCKTAIL_VR_MEASURE_END_NS` | unset | Limit `MOCKTAIL_VR_PERF` to a `CLOCK_MONOTONIC` window. |
|
||||
| `MOCKTAIL_VR_POSE_LOG_INTERVAL` | `240` | Frames between head-pose log lines. |
|
||||
|
||||
## Diagnostics builds
|
||||
|
||||
These only work in builds configured with `-DMOCKTAIL_VR_DIAGNOSTICS=ON`.
|
||||
Release builds ignore them.
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `MOCKTAIL_VR_POSE_SOURCE` | `runtime` | `script` drives the head from a scripted motion instead of the headset, for testing without wearing it. |
|
||||
| `MOCKTAIL_VR_POSE_AMPLITUDE_POSITION`, `_YAW`, `_PITCH` | `0.25`, `0.5`, `0.17` | Scripted motion size, in metres and radians. |
|
||||
| `MOCKTAIL_VR_POSE_PERIOD_POSITION`, `_ROTATION` | `240`, `360` | Scripted motion period, in frames. |
|
||||
| `MOCKTAIL_VR_XR_EVIDENCE_DIR` | unset | Writes submitted eye images to this directory. |
|
||||
| `MOCKTAIL_VR_XR_EVIDENCE_INTERVAL` | `20` | Frames between evidence images. |
|
||||
|
||||
## Adding a Roblox build
|
||||
|
||||
VR needs addresses that are specific to each Roblox build. To use a supported
|
||||
build that is not the active one, point `--roblox-lib` at its `libroblox.so`.
|
||||
|
||||
To add a build, run the offset finder on its library:
|
||||
|
||||
```bash
|
||||
python3 scripts/find_roblox_vr_offsets.py --elf <libroblox.so> --output offsets.json
|
||||
```
|
||||
|
||||
It needs the `capstone` Python package. Add its values to `kContracts` in
|
||||
`src/vr/roblox_vr_device_bridge.cc`. Nightcap checks those addresses against
|
||||
Roblox's code before it hooks anything, and leaves VR off if they do not
|
||||
match. To run the same checks on real libraries, set
|
||||
`MOCKTAIL_TEST_VR_PAYLOADS` to a folder of `<build>-<build id>/libroblox.so`
|
||||
payloads and run `ctest -R VrSupportedBuilds`.
|
||||
|
||||
For a second opinion, solve the new build from the previous one and compare
|
||||
the two answers. It needs the previous build's HostAbi profile and
|
||||
compatibility manifest:
|
||||
|
||||
```bash
|
||||
python3 scripts/roblox_abi_check.py --reference-lib <old libroblox.so> \
|
||||
--reference-profile <old profile.json> \
|
||||
--reference-compatibility config/roblox_compatibility.json \
|
||||
--candidate-lib <new libroblox.so> --vr
|
||||
```
|
||||
|
||||
It lists every field where the finder and the solver disagree and exits 0
|
||||
when they agree on all of them.
|
||||
|
||||
## Testing without a headset
|
||||
|
||||
[Monado](https://monado.freedesktop.org) can stand in for a headset. Its
|
||||
`qwerty` driver gives a head and two controllers that you move with the
|
||||
keyboard and mouse:
|
||||
|
||||
```bash
|
||||
QWERTY_ENABLE=1 XRT_DEBUG_GUI=1 monado-service
|
||||
```
|
||||
|
||||
Then, in another terminal:
|
||||
|
||||
```bash
|
||||
XR_RUNTIME_JSON=/usr/share/openxr/1/openxr_monado.json ./build/mocktail --vr
|
||||
```
|
||||
|
||||
The keys work while Monado's debug window has focus. The simulated display is
|
||||
small, so text in the headset view looks coarse.
|
||||
|
||||
Two tests need an OpenXR runtime and skip without one. To run them, start
|
||||
Monado with no window and opt in:
|
||||
|
||||
```bash
|
||||
XRT_COMPOSITOR_NULL=1 monado-service &
|
||||
XR_RUNTIME_JSON=/usr/share/openxr/1/openxr_monado.json MOCKTAIL_TEST_XR_GLES=1 \
|
||||
ctest --test-dir build -R GlesVrTransportTest.OpenXr
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "compat/bionic_host_libc_runtime.h"
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace mocktail::compat {
|
||||
// Heap accounting is a sampled statistic. A glibc scan walks free lists while
|
||||
// holding arena locks, so back-to-back Android queries share a recent sample.
|
||||
// The scan runs outside this mutex; concurrent callers can use the last complete
|
||||
// sample without waiting for another allocator walk.
|
||||
class AllocatorSnapshotCache {
|
||||
public:
|
||||
static constexpr std::uint64_t kRefreshNs=500'000'000ULL;
|
||||
template<class Reader>
|
||||
BionicMallinfoSnapshot Read(std::uint64_t now,Reader reader) noexcept {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
if(refreshing_ || (initialized_ && now<refresh_after_))return snapshot_;
|
||||
refreshing_=true;
|
||||
lock.unlock();
|
||||
const auto fresh=reader();
|
||||
lock.lock();
|
||||
snapshot_=fresh;
|
||||
initialized_=true;
|
||||
refresh_after_=now+kRefreshNs;
|
||||
refreshing_=false;
|
||||
return snapshot_;
|
||||
}
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
BionicMallinfoSnapshot snapshot_{};
|
||||
bool initialized_=false,refreshing_=false;
|
||||
std::uint64_t refresh_after_=0;
|
||||
};
|
||||
}
|
||||
@@ -44,7 +44,10 @@ class RobloxOutputDeviceBridge final {
|
||||
std::string_view logical_name,
|
||||
std::uintptr_t image_base);
|
||||
static int GetOutputDeviceCount(void* self);
|
||||
static void* GetOutputDeviceInfo(void* result, void* self, int index);
|
||||
// A trivial aggregate lets the compiler implement the guest's platform
|
||||
// sret convention (x8 on AArch64, hidden first argument on x86-64).
|
||||
struct alignas(8) DeviceInfoResult { std::uint8_t bytes[56]{}; };
|
||||
static DeviceInfoResult GetOutputDeviceInfo(void* self, int index);
|
||||
static int GetCurrentOutputDevice(void* self);
|
||||
static void SetCurrentOutputDevice(void* self, int index);
|
||||
|
||||
@@ -52,7 +55,7 @@ class RobloxOutputDeviceBridge final {
|
||||
Status PatchVtableLocked();
|
||||
bool RestoreVtableLocked();
|
||||
static int GetInputDeviceCount(void *self);
|
||||
static void *GetInputDeviceInfo(void *result, void *self, int index);
|
||||
static DeviceInfoResult GetInputDeviceInfo(void* self, int index);
|
||||
static int GetCurrentInputDevice(void *self);
|
||||
static void SetCurrentInputDevice(void *self, int index);
|
||||
int DeviceCount(bool input = false);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::graphics::internal {
|
||||
|
||||
// Only transfers between two concrete queue families can be converted for a
|
||||
// desktop swapchain. External/foreign ownership must reach the driver intact.
|
||||
inline bool NeedsDesktopQueueNormalization(std::uint32_t source,
|
||||
std::uint32_t destination) {
|
||||
return source != destination && source < VK_QUEUE_FAMILY_FOREIGN_EXT &&
|
||||
destination < VK_QUEUE_FAMILY_FOREIGN_EXT;
|
||||
}
|
||||
|
||||
template <class Barrier>
|
||||
inline bool HasDesktopQueueTransfers(std::uint32_t count,
|
||||
const Barrier* barriers) {
|
||||
if (!barriers) return false;
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
if (NeedsDesktopQueueNormalization(barriers[i].srcQueueFamilyIndex,
|
||||
barriers[i].dstQueueFamilyIndex))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep the caller's array for the common no-ownership-transfer path. A batch
|
||||
// callback can inspect a mixed array under one backend lock; the original
|
||||
// array and every structure chained through pNext remain untouched.
|
||||
template <class Barrier, class Normalize, class Forward>
|
||||
inline void WithDesktopQueueBarriers(std::uint32_t count,
|
||||
const Barrier* barriers,
|
||||
Normalize&& normalize, Forward&& forward) {
|
||||
if (!HasDesktopQueueTransfers(count, barriers)) {
|
||||
forward(barriers);
|
||||
return;
|
||||
}
|
||||
constexpr std::size_t kStackCount = 8;
|
||||
if (count <= kStackCount) {
|
||||
std::array<Barrier, kStackCount> adjusted;
|
||||
std::copy_n(barriers, count, adjusted.data());
|
||||
normalize(count, adjusted.data());
|
||||
forward(adjusted.data());
|
||||
} else {
|
||||
std::vector<Barrier> adjusted(barriers, barriers + count);
|
||||
normalize(count, adjusted.data());
|
||||
forward(adjusted.data());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics::internal
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::graphics::internal {
|
||||
|
||||
struct FoveationDevicePolicy {
|
||||
bool density_offsets_enabled = true;
|
||||
};
|
||||
|
||||
// Capture before invoking a runtime/loader device callback: its temporary
|
||||
// feature-chain records must never be inspected after the call returns.
|
||||
inline FoveationDevicePolicy SnapshotFoveationDevicePolicy(
|
||||
const VkDeviceCreateInfo* info) {
|
||||
if (!info || info->sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO ||
|
||||
info->enabledExtensionCount > 4096 ||
|
||||
(info->enabledExtensionCount && !info->ppEnabledExtensionNames)) return {};
|
||||
bool offsets = false;
|
||||
for (std::uint32_t i = 0; i < info->enabledExtensionCount; ++i) {
|
||||
const char* name = info->ppEnabledExtensionNames[i];
|
||||
if (!name) return {};
|
||||
if (!std::strcmp(name, "VK_QCOM_fragment_density_map_offset") ||
|
||||
!std::strcmp(name, "VK_EXT_fragment_density_map_offset"))
|
||||
offsets = true;
|
||||
}
|
||||
std::array<const VkBaseInStructure*, 64> seen{};
|
||||
std::size_t count = 0;
|
||||
for (auto* node = static_cast<const VkBaseInStructure*>(info->pNext);
|
||||
node; node = node->pNext) {
|
||||
if (count == seen.size()) return {};
|
||||
for (std::size_t i = 0; i < count; ++i) if (seen[i] == node) return {};
|
||||
seen[count++] = node;
|
||||
if (node->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_OFFSET_FEATURES_EXT)
|
||||
offsets |= reinterpret_cast<const VkPhysicalDeviceFragmentDensityMapOffsetFeaturesEXT*>(node)
|
||||
->fragmentDensityMapOffset != VK_FALSE;
|
||||
}
|
||||
return {offsets};
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics::internal
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
// Retired multisample choices use the native profile's SMAA High default.
|
||||
constexpr int NativeAntiAliasingMode(std::int64_t value) {
|
||||
return value >= 0 && value <= 3 ? static_cast<int>(value) : 3;
|
||||
}
|
||||
} // namespace mocktail::vr::internal
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
|
||||
// Load once during startup, then keep file I/O and parsing on a worker. The
|
||||
// render thread only copies a completed snapshot under a short-held lock.
|
||||
template <typename T>
|
||||
class AsyncSettings {
|
||||
public:
|
||||
using Reader = std::function<T(const T&)>;
|
||||
AsyncSettings(T initial, Reader reader, std::chrono::milliseconds interval)
|
||||
: value_(initial), reader_(std::move(reader)), interval_(interval) {
|
||||
static_assert(std::is_trivially_copyable_v<T>);
|
||||
Refresh();
|
||||
worker_ = std::thread([this] {
|
||||
std::unique_lock<std::mutex> stop_lock(stop_mutex_);
|
||||
while (!stopped_) {
|
||||
if (wake_.wait_for(stop_lock, interval_, [this] { return stopped_; }))
|
||||
break;
|
||||
stop_lock.unlock();
|
||||
Refresh();
|
||||
stop_lock.lock();
|
||||
}
|
||||
});
|
||||
}
|
||||
~AsyncSettings() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(stop_mutex_);
|
||||
stopped_ = true;
|
||||
}
|
||||
wake_.notify_one();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
AsyncSettings(const AsyncSettings&) = delete;
|
||||
AsyncSettings& operator=(const AsyncSettings&) = delete;
|
||||
|
||||
T Get() const {
|
||||
std::lock_guard<std::mutex> lock(value_mutex_);
|
||||
return value_;
|
||||
}
|
||||
|
||||
private:
|
||||
void Refresh() {
|
||||
try {
|
||||
const T next = reader_(Get());
|
||||
std::lock_guard<std::mutex> lock(value_mutex_);
|
||||
value_ = next;
|
||||
} catch (...) {
|
||||
// A failed read retains the last completed settings. In particular,
|
||||
// parsing must never throw through a guest rendering callback.
|
||||
}
|
||||
}
|
||||
mutable std::mutex value_mutex_;
|
||||
T value_;
|
||||
Reader reader_;
|
||||
std::chrono::milliseconds interval_;
|
||||
std::mutex stop_mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopped_ = false;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::vr::internal
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef MOCKTAIL_VR_DESKTOP_MIRROR_H_
|
||||
#define MOCKTAIL_VR_DESKTOP_MIRROR_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
|
||||
struct DesktopMirrorDispatch {
|
||||
PFN_vkCmdPipelineBarrier barrier;
|
||||
PFN_vkCmdClearColorImage clear;
|
||||
PFN_vkCmdBlitImage blit;
|
||||
};
|
||||
|
||||
// Record the whole copy, including layout restoration, into the submission's
|
||||
// command buffer. The caller owns synchronization, begin/end and submission.
|
||||
inline void RecordDesktopMirrorCommands(
|
||||
VkCommandBuffer command, VkImage source, VkImageLayout source_layout,
|
||||
VkExtent2D source_extent, VkImage destination, VkExtent2D destination_extent,
|
||||
const DesktopMirrorDispatch& vk) {
|
||||
VkImageMemoryBarrier barriers[2]{};
|
||||
for (auto& barrier : barriers) {
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex =
|
||||
VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
|
||||
}
|
||||
barriers[0].image = source;
|
||||
barriers[0].oldLayout = source_layout;
|
||||
barriers[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barriers[1].image = destination;
|
||||
barriers[1].oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
barriers[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vk.barrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2,
|
||||
barriers);
|
||||
const VkClearColorValue black{{0.f, 0.f, 0.f, 1.f}};
|
||||
vk.clear(command, destination, barriers[1].newLayout, &black, 1,
|
||||
&barriers[1].subresourceRange);
|
||||
VkMemoryBarrier clear_barrier{};
|
||||
clear_barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
||||
clear_barrier.srcAccessMask = clear_barrier.dstAccessMask =
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vk.barrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &clear_barrier, 0, nullptr, 0,
|
||||
nullptr);
|
||||
const double scale =
|
||||
std::min(double(destination_extent.width) / source_extent.width,
|
||||
double(destination_extent.height) / source_extent.height);
|
||||
const int width = std::max(1, int(source_extent.width * scale));
|
||||
const int height = std::max(1, int(source_extent.height * scale));
|
||||
const int x = (int(destination_extent.width) - width) / 2;
|
||||
const int y = (int(destination_extent.height) - height) / 2;
|
||||
VkImageBlit region{};
|
||||
region.srcSubresource =
|
||||
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.srcOffsets[1] = {static_cast<int>(source_extent.width),
|
||||
static_cast<int>(source_extent.height), 1};
|
||||
region.dstOffsets[0] = {x, y, 0};
|
||||
region.dstOffsets[1] = {x + width, y + height, 1};
|
||||
vk.blit(command, source, barriers[0].newLayout, destination,
|
||||
barriers[1].newLayout, 1, ®ion, VK_FILTER_NEAREST);
|
||||
for (auto& barrier : barriers) {
|
||||
std::swap(barrier.oldLayout, barrier.newLayout);
|
||||
barrier.srcAccessMask = barrier.dstAccessMask;
|
||||
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
|
||||
}
|
||||
vk.barrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 2,
|
||||
barriers);
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr::internal
|
||||
|
||||
#endif // MOCKTAIL_VR_DESKTOP_MIRROR_H_
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef MOCKTAIL_VR_EYE_RESIZE_BOUNDARY_H_
|
||||
#define MOCKTAIL_VR_EYE_RESIZE_BOUNDARY_H_
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
// Eye getters return borrowed render targets. Replacing those targets after
|
||||
// either eye has been requested can invalidate work recorded earlier in the
|
||||
// same frame, even if all previously submitted GPU work has finished.
|
||||
class EyeResizeBoundary {
|
||||
public:
|
||||
void OnPresent() { presented_ = true; eyes_used_ = false; }
|
||||
void OnEyeGetter() { eyes_used_ = true; }
|
||||
bool CanResize() const { return presented_ && !eyes_used_; }
|
||||
private:
|
||||
bool presented_ = false;
|
||||
bool eyes_used_ = false;
|
||||
};
|
||||
} // namespace mocktail::vr::internal
|
||||
#endif
|
||||
@@ -0,0 +1,226 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
struct QuantizedGazeCenter {unsigned x=0,y=0;};
|
||||
inline QuantizedGazeCenter QuantizeGazeCenter(unsigned width,unsigned height,float x,float y) {
|
||||
if(!width || !height || !std::isfinite(x) || !std::isfinite(y))return {};
|
||||
return {unsigned(std::round((std::clamp(x,-1.f,1.f)+1.f)*.5f*width*4.f)),
|
||||
unsigned(std::round((1.f-std::clamp(y,-1.f,1.f))*.5f*height*4.f))};
|
||||
}
|
||||
inline float ClampGazeRadius(float radius) {
|
||||
return std::isfinite(radius)?std::clamp(radius,.08f,.35f):.22f;
|
||||
}
|
||||
inline bool ProjectGazeNdc(const float* gaze,const float* eye,const float* fov,float* x,float* y) {
|
||||
if(!gaze || !eye || !fov || !x || !y)return false;
|
||||
auto rotate=[](const float* q,const float* v,float* out) {
|
||||
float length=0;for(int i=0;i<4;++i){if(!std::isfinite(q[i]))return false;length+=q[i]*q[i];}
|
||||
if(length<1e-8f)return false;
|
||||
const float inverse=1.f/std::sqrt(length);
|
||||
const float a=q[0]*inverse,b=q[1]*inverse,c=q[2]*inverse,d=q[3]*inverse;
|
||||
const float tx=2.f*(b*v[2]-c*v[1]),ty=2.f*(c*v[0]-a*v[2]),tz=2.f*(a*v[1]-b*v[0]);
|
||||
out[0]=v[0]+d*tx+b*tz-c*ty;out[1]=v[1]+d*ty+c*tx-a*tz;out[2]=v[2]+d*tz+a*ty-b*tx;
|
||||
return true;
|
||||
};
|
||||
const float forward[3]={0,0,-1};float local[3],world[3];
|
||||
const float inverse_eye[4]={-eye[0],-eye[1],-eye[2],eye[3]};
|
||||
if(!rotate(gaze,forward,world) || !rotate(inverse_eye,world,local) || local[2]>=-1e-5f)return false;
|
||||
const float left=std::tan(fov[0]),right=std::tan(fov[1]),up=std::tan(fov[2]),down=std::tan(fov[3]);
|
||||
if(!std::isfinite(left+right+up+down) || right<=left || up<=down)return false;
|
||||
*x=2.f*(local[0]/-local[2]-left)/(right-left)-1.f;
|
||||
*y=2.f*(local[1]/-local[2]-down)/(up-down)-1.f;
|
||||
return std::isfinite(*x)&&std::isfinite(*y);
|
||||
}
|
||||
// OpenXR eye-foveation centers use NDC (+Y up). Vulkan framebuffer/image rows
|
||||
// run downwards. Preserve a generous full-density disk around the actual gaze;
|
||||
// reduce shading only outside it. Density maps store reciprocal fragment size.
|
||||
inline void FillGazeDensityMap(std::vector<std::uint8_t>& pixels,unsigned width,unsigned height,
|
||||
float ndc_x,float ndc_y,int level=2,float radius=.22f,
|
||||
float region_half_width=0.f,float region_half_height=0.f) {
|
||||
pixels.assign(std::size_t(width)*height*2,255);
|
||||
if(!width || !height || !std::isfinite(ndc_x) || !std::isfinite(ndc_y) || level<=0)return;
|
||||
// Quarter-cell quantization avoids uploading new maps for tiny gaze jitter.
|
||||
// Its maximum position error is well inside the cell-diagonal guard below.
|
||||
const auto center=QuantizeGazeCenter(width,height,ndc_x,ndc_y);
|
||||
const float cx=center.x/(width*4.f),cy=center.y/(height*4.f);
|
||||
const float inner=ClampGazeRadius(radius);
|
||||
// Performance reduces detail sooner outside the same protected focus disk.
|
||||
const float outer=inner+(level==1?.36f:level==2?.26f:.10f);
|
||||
const float peripheral_work=level==1?.25f:.0625f;
|
||||
// A scalar ramp is quantized by the GPU into a few hard rings. Mix adjacent
|
||||
// supported fragment areas over a stable spatial pattern instead. This gives
|
||||
// intermediate average shading densities without temporal random noise or a
|
||||
// second full-resolution render. Alternate the elongated fragment direction
|
||||
// so intermediate stages do not consistently favour horizontal edges.
|
||||
constexpr float work[]={1.f,.5f,.25f,.125f,.0625f};
|
||||
constexpr unsigned sizes[][2]={{1,1},{1,2},{2,2},{2,4},{4,4}};
|
||||
constexpr unsigned threshold[4][4]={{0,8,2,10},{12,4,14,6},{3,11,1,9},{15,7,13,5}};
|
||||
// Each map cell covers an area, not a point. Extend the full-detail region
|
||||
// by a cell diagonal so a gaze near a cell boundary cannot land in a coarse
|
||||
// fragment just outside the mathematical disk.
|
||||
const float margin=std::hypot(1.f/width,1.f/height);
|
||||
const float inner_squared=(inner+margin)*(inner+margin);
|
||||
const float outer_squared=(outer+margin)*(outer+margin);
|
||||
const auto peripheral_density=static_cast<std::uint8_t>(level==1?127:63);
|
||||
for(unsigned y=0;y<height;++y)for(unsigned x=0;x<width;++x) {
|
||||
// A tiled GPU samples density at its region centre and applies it to the
|
||||
// entire region. Protect every region that could intersect the focus, not
|
||||
// just the tiny map cell containing the gaze. The bound comes from the
|
||||
// device's maxFragmentDensityTexelSize in framebuffer pixels.
|
||||
const float dx=std::max(0.f,std::abs((x+.5f)/width-cx)-region_half_width);
|
||||
const float dy=std::max(0.f,std::abs((y+.5f)/height-cy)-region_half_height);
|
||||
// Flat regions need no square root or density-stage search. The output
|
||||
// was initialized to full detail above; the peripheral reciprocal is exact.
|
||||
// Keep the existing hypot/ramp calculation throughout the transition.
|
||||
const float squared=dx*dx+dy*dy;
|
||||
if(squared<=inner_squared)continue;
|
||||
if(squared>=outer_squared) {
|
||||
const auto index=(std::size_t(y)*width+x)*2;
|
||||
pixels[index]=pixels[index+1]=peripheral_density;
|
||||
continue;
|
||||
}
|
||||
const float distance=std::hypot(dx,dy);
|
||||
float t=std::clamp((distance-inner-margin)/(outer-inner),0.f,1.f);
|
||||
t=t*t*(3.f-2.f*t);
|
||||
const float target=1.f-t*(1.f-peripheral_work);
|
||||
unsigned stage=0;
|
||||
while(stage<4 && target<work[stage+1])++stage;
|
||||
if(stage<4) {
|
||||
const float blend=(work[stage]-target)/(work[stage]-work[stage+1]);
|
||||
if(blend>(threshold[y%4][x%4]+.5f)/16.f)++stage;
|
||||
}
|
||||
const unsigned flip=(x+y)&1u;
|
||||
const auto index=(std::size_t(y)*width+x)*2;
|
||||
// Round density down: 128/255 requests an area smaller than 2 pixels,
|
||||
// which a conforming driver may clamp to 1; 127/255 actually permits 2.
|
||||
pixels[index]=static_cast<std::uint8_t>(255u/sizes[stage][flip]);
|
||||
pixels[index+1]=static_cast<std::uint8_t>(255u/sizes[stage][1u-flip]);
|
||||
}
|
||||
}
|
||||
inline std::vector<std::uint8_t> GazeDensityMap(unsigned width,unsigned height,
|
||||
float ndc_x,float ndc_y,int level=2,float radius=.22f,
|
||||
float region_half_width=0.f,float region_half_height=0.f) {
|
||||
std::vector<std::uint8_t> pixels;
|
||||
FillGazeDensityMap(pixels,width,height,ndc_x,ndc_y,level,radius,region_half_width,region_half_height);
|
||||
return pixels;
|
||||
}
|
||||
|
||||
struct FramebufferDensityLayout {
|
||||
unsigned map_width=0,map_height=0,width=0,height=0;
|
||||
unsigned texel_width=0,texel_height=0,active_width=0,active_height=0;
|
||||
bool valid=false;
|
||||
};
|
||||
// Turnip samples at pixel>>ceil(log2(framebuffer/map)), clamped to the
|
||||
// advertised power-of-two texel limits. Allocation padding is not scene UV.
|
||||
inline FramebufferDensityLayout TurnipDensityLayout(unsigned map_width,unsigned map_height,
|
||||
unsigned width,unsigned height,unsigned min_width,unsigned min_height,
|
||||
unsigned max_width,unsigned max_height) {
|
||||
FramebufferDensityLayout result{map_width,map_height,width,height};
|
||||
const auto power=[](unsigned value){return value && !(value&(value-1));};
|
||||
if(!map_width || !map_height || !width || !height || !power(min_width) ||
|
||||
!power(min_height) || !power(max_width) || !power(max_height) ||
|
||||
min_width>max_width || min_height>max_height)return result;
|
||||
const auto step=[](unsigned pixels,unsigned cells,unsigned minimum,unsigned maximum) {
|
||||
unsigned value=minimum;
|
||||
while(value<maximum && value<pixels/cells)value*=2;
|
||||
return value;
|
||||
};
|
||||
result.texel_width=step(width,map_width,min_width,max_width);
|
||||
result.texel_height=step(height,map_height,min_height,max_height);
|
||||
result.active_width=1+(width-1)/result.texel_width;
|
||||
result.active_height=1+(height-1)/result.texel_height;
|
||||
result.valid=result.active_width<=map_width && result.active_height<=map_height;
|
||||
return result;
|
||||
}
|
||||
inline QuantizedGazeCenter QuantizeFramebufferGaze(const FramebufferDensityLayout& layout,
|
||||
float x,float y) {
|
||||
if(!layout.valid || !std::isfinite(x) || !std::isfinite(y))return {};
|
||||
return {unsigned(std::round((std::clamp(x,-1.f,1.f)+1.f)*.5f*layout.width*4.f/layout.texel_width)),
|
||||
unsigned(std::round((1.f-std::clamp(y,-1.f,1.f))*.5f*layout.height*4.f/layout.texel_height))};
|
||||
}
|
||||
struct GazeDensityMapKey {
|
||||
unsigned x=0,y=0,width=0,height=0,map_width=0,map_height=0,texel_width=0,texel_height=0;
|
||||
int level=0;float radius=0;
|
||||
unsigned guard_width=0,guard_height=0,color_format=0,depth_format=0,samples=0,attachments=0;
|
||||
bool operator==(const GazeDensityMapKey& other) const {
|
||||
return x==other.x && y==other.y && width==other.width && height==other.height &&
|
||||
map_width==other.map_width && map_height==other.map_height &&
|
||||
texel_width==other.texel_width && texel_height==other.texel_height &&
|
||||
level==other.level && radius==other.radius &&
|
||||
guard_width==other.guard_width && guard_height==other.guard_height &&
|
||||
color_format==other.color_format && depth_format==other.depth_format &&
|
||||
samples==other.samples && attachments==other.attachments;
|
||||
}
|
||||
};
|
||||
enum class GazeDensityCacheAction { kUpload,kReuse,kNeutral };
|
||||
inline GazeDensityCacheAction DensityCacheAction(bool initialized,bool cached,
|
||||
std::uint64_t uploaded_frame,std::uint64_t frame,const GazeDensityMapKey& previous,
|
||||
const GazeDensityMapKey& requested) {
|
||||
if(initialized && cached && previous==requested)return GazeDensityCacheAction::kReuse;
|
||||
// A map already referenced in this frame cannot safely be overwritten for
|
||||
// another extent. Its pass uses the neutral map until the next frame.
|
||||
if(initialized && uploaded_frame==frame)return GazeDensityCacheAction::kNeutral;
|
||||
return GazeDensityCacheAction::kUpload;
|
||||
}
|
||||
inline bool FillFramebufferGazeDensityMap(std::vector<std::uint8_t>& pixels,
|
||||
const FramebufferDensityLayout& layout,float ndc_x,float ndc_y,int level,float radius,
|
||||
float region_half_width_pixels,float region_half_height_pixels) {
|
||||
pixels.assign(std::size_t(layout.map_width)*layout.map_height*2,255);
|
||||
if(!layout.valid || !std::isfinite(ndc_x) || !std::isfinite(ndc_y) ||
|
||||
!std::isfinite(region_half_width_pixels) || !std::isfinite(region_half_height_pixels) ||
|
||||
region_half_width_pixels<0 || region_half_height_pixels<0)return false;
|
||||
if(level<=0)return true;
|
||||
const auto center=QuantizeFramebufferGaze(layout,ndc_x,ndc_y);
|
||||
const float cx=float(center.x)*layout.texel_width/(4.f*layout.width);
|
||||
const float cy=float(center.y)*layout.texel_height/(4.f*layout.height);
|
||||
const float guard_x=region_half_width_pixels/layout.width;
|
||||
const float guard_y=region_half_height_pixels/layout.height;
|
||||
const float inner=ClampGazeRadius(radius);
|
||||
const float outer=inner+(level==1?.36f:level==2?.26f:.10f);
|
||||
const float margin=std::hypot(float(layout.texel_width)/layout.width,
|
||||
float(layout.texel_height)/layout.height);
|
||||
const float inner_squared=(inner+margin)*(inner+margin);
|
||||
const float outer_squared=(outer+margin)*(outer+margin);
|
||||
const float peripheral_work=level==1?.25f:.0625f;
|
||||
const auto peripheral_density=static_cast<std::uint8_t>(level==1?127:63);
|
||||
constexpr float work[]={1.f,.5f,.25f,.125f,.0625f};
|
||||
constexpr unsigned sizes[][2]={{1,1},{1,2},{2,2},{2,4},{4,4}};
|
||||
constexpr unsigned threshold[4][4]={{0,8,2,10},{12,4,14,6},{3,11,1,9},{15,7,13,5}};
|
||||
for(unsigned y=0;y<layout.active_height;++y)for(unsigned x=0;x<layout.active_width;++x) {
|
||||
const float px=(x+.5f)*layout.texel_width/layout.width;
|
||||
const float py=(y+.5f)*layout.texel_height/layout.height;
|
||||
const float dx=std::max(0.f,std::abs(px-cx)-guard_x);
|
||||
const float dy=std::max(0.f,std::abs(py-cy)-guard_y);
|
||||
const float squared=dx*dx+dy*dy;
|
||||
if(squared<=inner_squared)continue;
|
||||
const auto index=(std::size_t(y)*layout.map_width+x)*2;
|
||||
if(squared>=outer_squared) {pixels[index]=pixels[index+1]=peripheral_density;continue;}
|
||||
float t=std::clamp((std::hypot(dx,dy)-inner-margin)/(outer-inner),0.f,1.f);
|
||||
t=t*t*(3.f-2.f*t);
|
||||
const float target=1.f-t*(1.f-peripheral_work);
|
||||
unsigned stage=0;
|
||||
while(stage<4 && target<work[stage+1])++stage;
|
||||
if(stage<4 && (work[stage]-target)/(work[stage]-work[stage+1])>
|
||||
(threshold[y%4][x%4]+.5f)/16.f)++stage;
|
||||
const unsigned flip=(x+y)&1u;
|
||||
pixels[index]=static_cast<std::uint8_t>(255u/sizes[stage][flip]);
|
||||
pixels[index+1]=static_cast<std::uint8_t>(255u/sizes[stage][1u-flip]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// The driver advertises R16G16_SFLOAT density attachments. These exact binary
|
||||
// reciprocals avoid UNORM rounding and cover every area generated above.
|
||||
inline void FillHalfDensityMap(std::vector<std::uint16_t>& result,const std::vector<std::uint8_t>& pixels) {
|
||||
result.resize(pixels.size());
|
||||
for(std::size_t i=0;i<pixels.size();++i)
|
||||
result[i]=pixels[i]==127?0x3800u:pixels[i]==63?0x3400u:0x3c00u;
|
||||
}
|
||||
inline std::vector<std::uint16_t> HalfDensityMap(const std::vector<std::uint8_t>& pixels) {
|
||||
std::vector<std::uint16_t> result;
|
||||
FillHalfDensityMap(result,pixels);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
inline bool IsFoveationSceneSampleCount(VkSampleCountFlagBits samples) {
|
||||
return samples == VK_SAMPLE_COUNT_1_BIT || samples == VK_SAMPLE_COUNT_2_BIT ||
|
||||
samples == VK_SAMPLE_COUNT_4_BIT || samples == VK_SAMPLE_COUNT_8_BIT;
|
||||
}
|
||||
|
||||
// Preserve the native MSAA attachments and resolve. Only the extra density map
|
||||
// is single-sampled; changing the scene images or pipeline sample count here
|
||||
// would invalidate native framebuffers and memory allocations.
|
||||
inline bool IsFoveationScenePassCompatible(const VkRenderPassCreateInfo& info) {
|
||||
if (info.pNext || info.flags || info.subpassCount != 1 || !info.pSubpasses ||
|
||||
!info.pAttachments) {
|
||||
return false;
|
||||
}
|
||||
const auto& subpass = info.pSubpasses[0];
|
||||
if (subpass.flags || subpass.pipelineBindPoint != VK_PIPELINE_BIND_POINT_GRAPHICS ||
|
||||
subpass.colorAttachmentCount != 1 || !subpass.pColorAttachments ||
|
||||
!subpass.pDepthStencilAttachment || subpass.inputAttachmentCount ||
|
||||
subpass.preserveAttachmentCount) {
|
||||
return false;
|
||||
}
|
||||
const auto color = subpass.pColorAttachments[0].attachment;
|
||||
const auto depth = subpass.pDepthStencilAttachment->attachment;
|
||||
if (color >= info.attachmentCount || depth >= info.attachmentCount || color == depth) {
|
||||
return false;
|
||||
}
|
||||
const auto samples = info.pAttachments[color].samples;
|
||||
if (!IsFoveationSceneSampleCount(samples) || info.pAttachments[depth].samples != samples) {
|
||||
return false;
|
||||
}
|
||||
const auto resolve = subpass.pResolveAttachments ?
|
||||
subpass.pResolveAttachments[0].attachment : VK_ATTACHMENT_UNUSED;
|
||||
if (resolve != VK_ATTACHMENT_UNUSED) {
|
||||
if (samples == VK_SAMPLE_COUNT_1_BIT || info.attachmentCount != 3 ||
|
||||
resolve >= info.attachmentCount || resolve == color || resolve == depth ||
|
||||
info.pAttachments[resolve].samples != VK_SAMPLE_COUNT_1_BIT ||
|
||||
info.pAttachments[resolve].format != info.pAttachments[color].format) {
|
||||
return false;
|
||||
}
|
||||
} else if (info.attachmentCount != 2) {
|
||||
return false;
|
||||
}
|
||||
for (unsigned i = 0; i < info.attachmentCount; ++i) {
|
||||
if (info.pAttachments[i].flags) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
struct FoveationTileDriverIdentity {
|
||||
std::uint32_t vendor_id = 0;
|
||||
std::uint32_t device_id = 0;
|
||||
std::uint32_t driver_id = 0;
|
||||
std::uint32_t driver_version = 0;
|
||||
std::string_view driver_name;
|
||||
std::string_view driver_info;
|
||||
std::array<std::uint8_t, 16> driver_uuid{};
|
||||
};
|
||||
|
||||
// Values describe the complete render pass, including its density attachment.
|
||||
// Attachment formats use VkFormat's numeric values without requiring Vulkan.
|
||||
struct FoveationTilePassIdentity {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t color_format = 0;
|
||||
std::uint32_t depth_format = 0;
|
||||
std::uint32_t color_samples = 0;
|
||||
std::uint32_t depth_samples = 0;
|
||||
std::uint32_t framebuffer_layers = 0;
|
||||
std::uint32_t subpass_count = 0;
|
||||
std::uint32_t total_attachment_count = 0;
|
||||
std::uint32_t color_attachment_count = 0;
|
||||
std::uint32_t gmem_attachment_count = 0;
|
||||
std::uint32_t resolve_attachment_mask = 0;
|
||||
std::uint32_t input_attachment_mask = 0;
|
||||
std::uint32_t feedback_attachment_mask = 0;
|
||||
std::uint32_t view_mask = 0;
|
||||
bool has_depth_attachment = false;
|
||||
bool has_density_attachment = false;
|
||||
bool density_offsets_enabled = false;
|
||||
bool multisampled_render_to_single_sampled = false;
|
||||
bool custom_resolve = false;
|
||||
bool has_shading_rate_attachment = false;
|
||||
};
|
||||
|
||||
struct FoveationTileBound {
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
};
|
||||
|
||||
// A calibrated entry for one driver and pass tuple, not a hardware maximum.
|
||||
// Unknown identities retain the caller's conservative policy. This exact entry
|
||||
// passed 588 GPU focus cases; the matching driver ELF forces FDM's AVOID_CCU
|
||||
// layout for all calibrated load/store combinations and the autotune path.
|
||||
inline std::optional<FoveationTileBound> KnownFoveationTileBound(
|
||||
const FoveationTileDriverIdentity& driver,
|
||||
const FoveationTilePassIdentity& pass) {
|
||||
constexpr std::array<std::uint8_t, 16> kDriverUuid{
|
||||
0x58, 0x6b, 0x9f, 0xa4, 0x51, 0x4c, 0xb8, 0x8b,
|
||||
0x3c, 0x11, 0xa9, 0x68, 0xef, 0x4d, 0x62, 0xe4};
|
||||
if (driver.vendor_id != 0x5143 || driver.device_id != 0x43051401 ||
|
||||
driver.driver_id != 18 || driver.driver_version != 109060195 ||
|
||||
driver.driver_name != "turnip Mesa driver" ||
|
||||
driver.driver_info != "Mesa 26.3.0-devel (git-abc426bb62)" ||
|
||||
driver.driver_uuid != kDriverUuid) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (pass.width != 2160 || pass.height != 2160 ||
|
||||
pass.color_format != 37 || pass.depth_format != 126 ||
|
||||
pass.color_samples != 1 || pass.depth_samples != 1 ||
|
||||
pass.framebuffer_layers != 1 || pass.subpass_count != 1 ||
|
||||
pass.total_attachment_count != 3 || pass.color_attachment_count != 1 ||
|
||||
pass.gmem_attachment_count != 2 ||
|
||||
pass.resolve_attachment_mask || pass.input_attachment_mask ||
|
||||
pass.feedback_attachment_mask || pass.view_mask ||
|
||||
!pass.has_depth_attachment || !pass.has_density_attachment ||
|
||||
pass.density_offsets_enabled ||
|
||||
pass.multisampled_render_to_single_sampled || pass.custom_resolve ||
|
||||
pass.has_shading_rate_attachment) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return FoveationTileBound{768, 448};
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,298 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
struct Fsr3DeviceFeatureSupport {
|
||||
bool shader_float16 = false;
|
||||
bool shader_int16 = false;
|
||||
bool float16_extension_available = false;
|
||||
bool shader_storage_image_extended_formats = false;
|
||||
bool shader_storage_image_write_without_format = false;
|
||||
// The effective instance/device API version, not merely a header version.
|
||||
std::uint32_t api_version = VK_API_VERSION_1_0;
|
||||
};
|
||||
|
||||
// A call-scoped, owned device-create feature chain. This object must remain
|
||||
// alive until xrCreateVulkanDeviceKHR/vkCreateDevice returns. Queue descriptions
|
||||
// and layer names stay borrowed and unmodified, as in the original call.
|
||||
// Unsupported or unfamiliar chains return the original create info verbatim.
|
||||
class Fsr3DeviceFeatures final {
|
||||
// The runtime normalizer reuses the same typed ownership/whitelist without
|
||||
// relaxing the guest feature-chain validation performed by this class.
|
||||
friend class Fsr3RuntimeTimelineFeatures;
|
||||
public:
|
||||
Fsr3DeviceFeatures(const VkDeviceCreateInfo& original,
|
||||
const Fsr3DeviceFeatureSupport& support)
|
||||
: original_(original), info_(original) {
|
||||
Build(support);
|
||||
}
|
||||
Fsr3DeviceFeatures(const Fsr3DeviceFeatures&) = delete;
|
||||
Fsr3DeviceFeatures& operator=(const Fsr3DeviceFeatures&) = delete;
|
||||
Fsr3DeviceFeatures(Fsr3DeviceFeatures&&) = delete;
|
||||
Fsr3DeviceFeatures& operator=(Fsr3DeviceFeatures&&) = delete;
|
||||
|
||||
const VkDeviceCreateInfo& create_info() const {
|
||||
return enabled_ ? info_ : original_;
|
||||
}
|
||||
bool fp16_enabled() const { return enabled_; }
|
||||
bool changed() const { return enabled_ && changed_; }
|
||||
const char* reason() const { return reason_; }
|
||||
|
||||
private:
|
||||
struct Record {
|
||||
virtual ~Record() = default;
|
||||
virtual void* address() = 0;
|
||||
virtual void next(void*) = 0;
|
||||
};
|
||||
template<class T> struct TypedRecord final : Record {
|
||||
explicit TypedRecord(const T& source) : value(source) {}
|
||||
void* address() override { return &value; }
|
||||
void next(void* value_next) override { value.pNext = value_next; }
|
||||
T value;
|
||||
};
|
||||
template<class T> T* Clone(const T& value) {
|
||||
auto record = std::make_unique<TypedRecord<T>>(value);
|
||||
auto* result = &record->value;
|
||||
chain_.push_back(std::move(record));
|
||||
return result;
|
||||
}
|
||||
static constexpr std::uint64_t Bit(unsigned value) { return 1ull << value; }
|
||||
static constexpr std::uint64_t Range(unsigned first, unsigned last) {
|
||||
return ((1ull << (last + 1)) - 1) & ~((1ull << first) - 1);
|
||||
}
|
||||
|
||||
bool CloneKnown(const VkBaseInStructure* source, std::uint64_t* families) {
|
||||
std::uint64_t used = 0;
|
||||
#define FEATURE(type, tag, mask) \
|
||||
case tag: { \
|
||||
used = (mask); \
|
||||
if (*families & used) return false; \
|
||||
Clone(*reinterpret_cast<const type*>(source)); \
|
||||
break; \
|
||||
}
|
||||
switch (source->sType) {
|
||||
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2: {
|
||||
used = Bit(0);
|
||||
if (*families & used) return false;
|
||||
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceFeatures2*>(source));
|
||||
base_features_ = &value->features;
|
||||
features2_ = true;
|
||||
break;
|
||||
}
|
||||
FEATURE(VkPhysicalDeviceVulkan11Features,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, Range(1, 6))
|
||||
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES: {
|
||||
used = Range(7, 19);
|
||||
if (*families & used) return false;
|
||||
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceVulkan12Features*>(source));
|
||||
float16_ = &value->shaderFloat16;
|
||||
break;
|
||||
}
|
||||
FEATURE(VkPhysicalDeviceVulkan13Features,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, Range(20, 32))
|
||||
FEATURE(VkPhysicalDevice16BitStorageFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, Bit(1))
|
||||
FEATURE(VkPhysicalDeviceMultiviewFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES, Bit(2))
|
||||
FEATURE(VkPhysicalDeviceVariablePointersFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTERS_FEATURES, Bit(3))
|
||||
FEATURE(VkPhysicalDeviceProtectedMemoryFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROTECTED_MEMORY_FEATURES, Bit(4))
|
||||
FEATURE(VkPhysicalDeviceSamplerYcbcrConversionFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, Bit(5))
|
||||
FEATURE(VkPhysicalDeviceShaderDrawParametersFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, Bit(6))
|
||||
FEATURE(VkPhysicalDevice8BitStorageFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, Bit(7))
|
||||
FEATURE(VkPhysicalDeviceShaderAtomicInt64Features,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES, Bit(8))
|
||||
case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES: {
|
||||
used = Bit(9);
|
||||
if (*families & used) return false;
|
||||
auto* value = Clone(*reinterpret_cast<const VkPhysicalDeviceShaderFloat16Int8Features*>(source));
|
||||
float16_ = &value->shaderFloat16;
|
||||
break;
|
||||
}
|
||||
FEATURE(VkPhysicalDeviceDescriptorIndexingFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, Bit(10))
|
||||
FEATURE(VkPhysicalDeviceScalarBlockLayoutFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES, Bit(11))
|
||||
FEATURE(VkPhysicalDeviceImagelessFramebufferFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES, Bit(12))
|
||||
FEATURE(VkPhysicalDeviceUniformBufferStandardLayoutFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_UNIFORM_BUFFER_STANDARD_LAYOUT_FEATURES, Bit(13))
|
||||
FEATURE(VkPhysicalDeviceShaderSubgroupExtendedTypesFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_EXTENDED_TYPES_FEATURES, Bit(14))
|
||||
FEATURE(VkPhysicalDeviceSeparateDepthStencilLayoutsFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES, Bit(15))
|
||||
FEATURE(VkPhysicalDeviceHostQueryResetFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES, Bit(16))
|
||||
FEATURE(VkPhysicalDeviceTimelineSemaphoreFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES, Bit(17))
|
||||
FEATURE(VkPhysicalDeviceBufferDeviceAddressFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES, Bit(18))
|
||||
FEATURE(VkPhysicalDeviceVulkanMemoryModelFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES, Bit(19))
|
||||
FEATURE(VkPhysicalDeviceImageRobustnessFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES, Bit(20))
|
||||
FEATURE(VkPhysicalDeviceInlineUniformBlockFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_FEATURES, Bit(21))
|
||||
FEATURE(VkPhysicalDevicePipelineCreationCacheControlFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES, Bit(22))
|
||||
FEATURE(VkPhysicalDevicePrivateDataFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIVATE_DATA_FEATURES, Bit(23))
|
||||
FEATURE(VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES, Bit(24))
|
||||
FEATURE(VkPhysicalDeviceShaderTerminateInvocationFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES, Bit(25))
|
||||
FEATURE(VkPhysicalDeviceSubgroupSizeControlFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES, Bit(26))
|
||||
FEATURE(VkPhysicalDeviceSynchronization2Features,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES, Bit(27))
|
||||
FEATURE(VkPhysicalDeviceTextureCompressionASTCHDRFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXTURE_COMPRESSION_ASTC_HDR_FEATURES, Bit(28))
|
||||
FEATURE(VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES, Bit(29))
|
||||
FEATURE(VkPhysicalDeviceDynamicRenderingFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES, Bit(30))
|
||||
FEATURE(VkPhysicalDeviceShaderIntegerDotProductFeatures,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_DOT_PRODUCT_FEATURES, Bit(31))
|
||||
FEATURE(VkPhysicalDeviceMaintenance4Features,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_4_FEATURES, Bit(32))
|
||||
FEATURE(VkPhysicalDeviceFragmentDensityMapFeaturesEXT,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT, Bit(33))
|
||||
FEATURE(VkPhysicalDeviceFragmentDensityMap2FeaturesEXT,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_2_FEATURES_EXT, Bit(34))
|
||||
FEATURE(VkPhysicalDeviceHostImageCopyFeaturesEXT,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT, Bit(35))
|
||||
FEATURE(VkPhysicalDeviceFragmentShadingRateFeaturesKHR,
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_FEATURES_KHR, Bit(36))
|
||||
default: return false;
|
||||
}
|
||||
#undef FEATURE
|
||||
*families |= used;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CopyExtensionNames() {
|
||||
if (original_.enabledExtensionCount && !original_.ppEnabledExtensionNames)
|
||||
return false;
|
||||
for (std::uint32_t i = 0; i < original_.enabledExtensionCount; ++i) {
|
||||
const char* name = original_.ppEnabledExtensionNames[i];
|
||||
if (!name) return false;
|
||||
std::size_t length = 0;
|
||||
while (length < VK_MAX_EXTENSION_NAME_SIZE && name[length]) ++length;
|
||||
if (!length || length == VK_MAX_EXTENSION_NAME_SIZE) return false;
|
||||
const std::string value(name, length);
|
||||
if (std::find(extension_names_.begin(), extension_names_.end(), value) == extension_names_.end())
|
||||
extension_names_.push_back(value);
|
||||
else changed_ = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Build(const Fsr3DeviceFeatureSupport& support) {
|
||||
if (!support.shader_float16 || !support.shader_int16) {
|
||||
reason_ = "shaderFloat16 or shaderInt16 unsupported";
|
||||
return;
|
||||
}
|
||||
if (original_.sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO) {
|
||||
reason_ = "invalid device create info";
|
||||
return;
|
||||
}
|
||||
std::uint64_t families = 0;
|
||||
std::vector<const void*> seen;
|
||||
for (auto* next = static_cast<const VkBaseInStructure*>(original_.pNext);
|
||||
next; next = next->pNext) {
|
||||
if (seen.size() == 64 || std::find(seen.begin(), seen.end(), next) != seen.end() ||
|
||||
!CloneKnown(next, &families)) {
|
||||
reason_ = "unknown, cyclic, or duplicate feature chain";
|
||||
return;
|
||||
}
|
||||
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES)
|
||||
// The Vulkan11 aggregate structure itself was introduced in1.2.
|
||||
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_2);
|
||||
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES)
|
||||
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_2);
|
||||
if (next->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES)
|
||||
minimum_api_ = std::max(minimum_api_, VK_API_VERSION_1_3);
|
||||
seen.push_back(next);
|
||||
}
|
||||
if (features2_ && original_.pEnabledFeatures) {
|
||||
reason_ = "duplicate base feature records";
|
||||
return;
|
||||
}
|
||||
if (support.api_version < minimum_api_) {
|
||||
reason_ = "core feature record exceeds effective Vulkan API";
|
||||
return;
|
||||
}
|
||||
if (support.api_version < VK_API_VERSION_1_2 && !support.float16_extension_available) {
|
||||
reason_ = "shaderFloat16 extension unavailable";
|
||||
return;
|
||||
}
|
||||
if (!CopyExtensionNames()) {
|
||||
reason_ = "invalid device extension list";
|
||||
return;
|
||||
}
|
||||
if (support.api_version < VK_API_VERSION_1_2) {
|
||||
const std::string extension = VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME;
|
||||
if (std::find(extension_names_.begin(), extension_names_.end(), extension) == extension_names_.end()) {
|
||||
extension_names_.push_back(extension);
|
||||
changed_ = true;
|
||||
}
|
||||
}
|
||||
if (!float16_) {
|
||||
VkPhysicalDeviceShaderFloat16Int8Features value{};
|
||||
value.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES;
|
||||
float16_ = &Clone(value)->shaderFloat16;
|
||||
changed_ = true;
|
||||
}
|
||||
if (!base_features_) {
|
||||
if (original_.pEnabledFeatures) features_ = *original_.pEnabledFeatures;
|
||||
base_features_ = &features_;
|
||||
info_.pEnabledFeatures = base_features_;
|
||||
}
|
||||
changed_ |= *float16_ != VK_TRUE || base_features_->shaderInt16 != VK_TRUE;
|
||||
*float16_ = VK_TRUE;
|
||||
base_features_->shaderInt16 = VK_TRUE;
|
||||
if (support.shader_storage_image_extended_formats) {
|
||||
changed_ |= base_features_->shaderStorageImageExtendedFormats != VK_TRUE;
|
||||
base_features_->shaderStorageImageExtendedFormats = VK_TRUE;
|
||||
}
|
||||
if (support.shader_storage_image_write_without_format) {
|
||||
changed_ |= base_features_->shaderStorageImageWriteWithoutFormat != VK_TRUE;
|
||||
base_features_->shaderStorageImageWriteWithoutFormat = VK_TRUE;
|
||||
}
|
||||
for (std::size_t i = 0; i < chain_.size(); ++i)
|
||||
chain_[i]->next(i + 1 < chain_.size() ? chain_[i + 1]->address() : nullptr);
|
||||
info_.pNext = chain_.empty() ? nullptr : chain_[0]->address();
|
||||
for (const auto& name : extension_names_) extension_pointers_.push_back(name.c_str());
|
||||
info_.enabledExtensionCount = static_cast<std::uint32_t>(extension_pointers_.size());
|
||||
info_.ppEnabledExtensionNames = extension_pointers_.empty() ? nullptr : extension_pointers_.data();
|
||||
enabled_ = true;
|
||||
reason_ = "enabled";
|
||||
}
|
||||
|
||||
const VkDeviceCreateInfo& original_;
|
||||
VkDeviceCreateInfo info_;
|
||||
std::vector<std::unique_ptr<Record>> chain_;
|
||||
std::vector<std::string> extension_names_;
|
||||
std::vector<const char*> extension_pointers_;
|
||||
VkPhysicalDeviceFeatures features_{};
|
||||
VkPhysicalDeviceFeatures* base_features_ = nullptr;
|
||||
VkBool32* float16_ = nullptr;
|
||||
bool features2_ = false;
|
||||
std::uint32_t minimum_api_ = VK_API_VERSION_1_0;
|
||||
bool enabled_ = false;
|
||||
bool changed_ = false;
|
||||
const char* reason_ = "disabled";
|
||||
};
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include "mocktail/vr/fsr3_upscaler.h"
|
||||
#include <memory>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// A fixed-size temporal focus region with a cheap full-image periphery.
|
||||
// The owner retires pending GPU users before reusing descriptors or resetting.
|
||||
class Fsr3GazeTransport {
|
||||
public:
|
||||
struct BeginInfo {
|
||||
VkImage encoded_source{}; // TRANSFER_SRC_OPTIMAL; encoded UNORM or sRGB.
|
||||
Fsr3Upscaler::Image depth; // Full-input size, SHADER_READ_ONLY_OPTIMAL.
|
||||
Fsr3Upscaler::Image motion; // Optional native clip-NDC delta, same layout/size.
|
||||
float gaze_x=.5f, gaze_y=.5f; // Normalized image UV, +Y down.
|
||||
bool reset=false;
|
||||
};
|
||||
struct Dimensions {
|
||||
std::uint32_t input_width{},input_height{},output_width{},output_height{};
|
||||
};
|
||||
Fsr3GazeTransport();
|
||||
~Fsr3GazeTransport();
|
||||
Fsr3GazeTransport(const Fsr3GazeTransport&)=delete;
|
||||
Fsr3GazeTransport& operator=(const Fsr3GazeTransport&)=delete;
|
||||
bool Prepare(VkDevice,PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&,VkFormat encoded_format,
|
||||
std::uint32_t full_input_width,std::uint32_t full_input_height,
|
||||
std::uint32_t full_output_width,std::uint32_t full_output_height,
|
||||
float sharp_radius);
|
||||
bool Begin(VkCommandBuffer,int eye,const BeginInfo&);
|
||||
void Finish(VkCommandBuffer,int eye);
|
||||
Dimensions dimensions()const;
|
||||
float vertical_fov_scale()const;
|
||||
bool reset_history(int eye)const;
|
||||
Fsr3Upscaler::Image color(int eye)const;
|
||||
Fsr3Upscaler::Image depth(int eye)const;
|
||||
Fsr3Upscaler::Image motion(int eye)const;
|
||||
Fsr3Upscaler::Image output(int eye)const;
|
||||
VkImage encoded(int eye)const;
|
||||
VkFormat encoded_format()const;
|
||||
void Reset();
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include "mocktail/vr/fsr3_device_features.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// SteamVR can append a TimelineSemaphore feature record even when a guest
|
||||
// already supplied Vulkan12Features. Fold that single promoted feature into
|
||||
// its aggregate immediately before the host vkCreateDevice call. Every known
|
||||
// record is cloned so no borrowed pNext pointer is rewritten. Unknown records,
|
||||
// conflicting records, and malformed chains pass through verbatim.
|
||||
class Fsr3RuntimeTimelineFeatures final {
|
||||
public:
|
||||
explicit Fsr3RuntimeTimelineFeatures(const VkDeviceCreateInfo& original)
|
||||
: owned_(original, Fsr3DeviceFeatureSupport{}) {
|
||||
if (original.sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO) return;
|
||||
const VkPhysicalDeviceVulkan12Features* aggregate = nullptr;
|
||||
const VkPhysicalDeviceTimelineSemaphoreFeatures* timeline = nullptr;
|
||||
std::vector<const VkBaseInStructure*> records;
|
||||
for (auto* record = static_cast<const VkBaseInStructure*>(original.pNext);
|
||||
record; record = record->pNext) {
|
||||
if (records.size() == 64 ||
|
||||
std::find(records.begin(), records.end(), record) != records.end()) return;
|
||||
if (record->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES) {
|
||||
if (aggregate) return;
|
||||
aggregate = reinterpret_cast<const VkPhysicalDeviceVulkan12Features*>(record);
|
||||
}
|
||||
if (record->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES) {
|
||||
if (timeline) return;
|
||||
timeline = reinterpret_cast<const VkPhysicalDeviceTimelineSemaphoreFeatures*>(record);
|
||||
}
|
||||
records.push_back(record);
|
||||
}
|
||||
if (!aggregate || !timeline) return;
|
||||
std::uint64_t families = 0;
|
||||
VkPhysicalDeviceVulkan12Features* cloned_aggregate = nullptr;
|
||||
for (auto* record : records) {
|
||||
if (record == reinterpret_cast<const VkBaseInStructure*>(timeline)) continue;
|
||||
if (!owned_.CloneKnown(record, &families)) return;
|
||||
if (record == reinterpret_cast<const VkBaseInStructure*>(aggregate))
|
||||
cloned_aggregate = static_cast<VkPhysicalDeviceVulkan12Features*>(
|
||||
owned_.chain_.back()->address());
|
||||
}
|
||||
if (!cloned_aggregate || (owned_.features2_ && original.pEnabledFeatures)) return;
|
||||
cloned_aggregate->timelineSemaphore =
|
||||
aggregate->timelineSemaphore || timeline->timelineSemaphore ? VK_TRUE : VK_FALSE;
|
||||
for (std::size_t i = 0; i < owned_.chain_.size(); ++i)
|
||||
owned_.chain_[i]->next(i + 1 < owned_.chain_.size()
|
||||
? owned_.chain_[i + 1]->address() : nullptr);
|
||||
owned_.info_.pNext = owned_.chain_.front()->address();
|
||||
changed_ = true;
|
||||
}
|
||||
Fsr3RuntimeTimelineFeatures(const Fsr3RuntimeTimelineFeatures&) = delete;
|
||||
Fsr3RuntimeTimelineFeatures& operator=(const Fsr3RuntimeTimelineFeatures&) = delete;
|
||||
const VkDeviceCreateInfo& create_info() const {
|
||||
return changed_ ? owned_.info_ : owned_.original_;
|
||||
}
|
||||
bool changed() const { return changed_; }
|
||||
|
||||
private:
|
||||
Fsr3DeviceFeatures owned_;
|
||||
bool changed_ = false;
|
||||
};
|
||||
|
||||
// Only calls made in this thread's scope normalize vkCreateDevice. Retained
|
||||
// callbacks and calls on other threads use an atomic snapshot of the raw host
|
||||
// dispatch, without normalization. The backend must clear that fallback before
|
||||
// unloading its private host Vulkan library.
|
||||
class ScopedFsr3VulkanDeviceCreate final {
|
||||
public:
|
||||
ScopedFsr3VulkanDeviceCreate(PFN_vkGetInstanceProcAddr raw, VkInstance instance)
|
||||
: raw_(raw), previous_(current_) {
|
||||
if (raw_)
|
||||
create_ = reinterpret_cast<PFN_vkCreateDevice>(raw_(instance, "vkCreateDevice"));
|
||||
if (create_ && create_ != &CreateDevice) {
|
||||
current_ = this;
|
||||
std::shared_ptr<const RawDispatch> dispatch =
|
||||
std::make_shared<RawDispatch>(RawDispatch{raw_, create_});
|
||||
std::atomic_store_explicit(&fallback_, std::move(dispatch), std::memory_order_release);
|
||||
} else create_ = nullptr;
|
||||
}
|
||||
~ScopedFsr3VulkanDeviceCreate() {
|
||||
if (current_ == this) current_ = previous_;
|
||||
}
|
||||
ScopedFsr3VulkanDeviceCreate(const ScopedFsr3VulkanDeviceCreate&) = delete;
|
||||
ScopedFsr3VulkanDeviceCreate& operator=(const ScopedFsr3VulkanDeviceCreate&) = delete;
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr() const {
|
||||
return create_ ? &GetInstanceProcAddr : raw_;
|
||||
}
|
||||
bool active() const { return create_ != nullptr; }
|
||||
static void ClearFallback() {
|
||||
std::atomic_store_explicit(&fallback_, std::shared_ptr<const RawDispatch>{},
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
private:
|
||||
struct RawDispatch {
|
||||
PFN_vkGetInstanceProcAddr gipa = nullptr;
|
||||
PFN_vkCreateDevice create = nullptr;
|
||||
};
|
||||
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetInstanceProcAddr(
|
||||
VkInstance instance, const char* name) {
|
||||
auto* scope = current_;
|
||||
if (!scope || !scope->raw_) {
|
||||
const auto dispatch = std::atomic_load_explicit(&fallback_, std::memory_order_acquire);
|
||||
return dispatch && dispatch->gipa ? dispatch->gipa(instance, name) : nullptr;
|
||||
}
|
||||
if (name && std::strcmp(name, "vkCreateDevice") == 0)
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(&CreateDevice);
|
||||
return scope->raw_(instance, name);
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(
|
||||
VkPhysicalDevice physical, const VkDeviceCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator, VkDevice* device) {
|
||||
auto* scope = current_;
|
||||
if (!scope || !scope->create_) {
|
||||
const auto dispatch = std::atomic_load_explicit(&fallback_, std::memory_order_acquire);
|
||||
return dispatch && dispatch->create
|
||||
? dispatch->create(physical, info, allocator, device)
|
||||
: VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
if (!info) return scope->create_(physical, info, allocator, device);
|
||||
Fsr3RuntimeTimelineFeatures normalized(*info);
|
||||
return scope->create_(physical, &normalized.create_info(), allocator, device);
|
||||
}
|
||||
inline static thread_local ScopedFsr3VulkanDeviceCreate* current_ = nullptr;
|
||||
inline static std::shared_ptr<const RawDispatch> fallback_;
|
||||
PFN_vkGetInstanceProcAddr raw_ = nullptr;
|
||||
PFN_vkCreateDevice create_ = nullptr;
|
||||
ScopedFsr3VulkanDeviceCreate* previous_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "mocktail/vr/fsr3_scene_inputs.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
// Owns depth snapshots, recorded before the guest reuses its scene depth.
|
||||
// All methods are serialized by the backend and use its single graphics queue.
|
||||
class Fsr3SceneCapture {
|
||||
public:
|
||||
struct Eye {
|
||||
VkImage depth{};
|
||||
VkDeviceMemory memory{};
|
||||
VkFormat format{};
|
||||
std::uint32_t width{},height{};
|
||||
std::uint64_t submitted_frame{};
|
||||
bool initialized{};
|
||||
};
|
||||
struct Motion {
|
||||
VkImage image{};VkDeviceMemory memory{};VkFormat format{};
|
||||
std::uint32_t width{},height{};std::uint64_t submitted_frame{};
|
||||
float previous_uv_scale_x=-.5f,previous_uv_scale_y=.5f;
|
||||
};
|
||||
~Fsr3SceneCapture() { Reset(); }
|
||||
bool NeedsResize(const TemporalSceneInputs&) const;
|
||||
bool Record(VkDevice, PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&, VkCommandBuffer,
|
||||
const TemporalSceneInputs&, std::uint64_t frame);
|
||||
bool RecordMotion(VkDevice, PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&, VkCommandBuffer,
|
||||
const TemporalMotionInputs&, std::uint64_t frame);
|
||||
void Submitted(std::uint32_t,const VkCommandBuffer*);
|
||||
void Discard(VkCommandBuffer);
|
||||
bool HasUnsubmitted() const;
|
||||
const Eye* Get(int eye,std::uint64_t frame,std::uint32_t width,
|
||||
std::uint32_t height) const;
|
||||
const Motion* GetMotion(int eye,std::uint64_t frame,std::uint32_t width,
|
||||
std::uint32_t height) const;
|
||||
void Reset();
|
||||
private:
|
||||
template<class T> T Proc(const char* name) const {
|
||||
return reinterpret_cast<T>(proc_(device_,name));
|
||||
}
|
||||
bool Create(Eye&,const TemporalSceneInputs&,
|
||||
const VkPhysicalDeviceMemoryProperties&);
|
||||
bool Allocate(VkImage&,VkDeviceMemory&,VkFormat,std::uint32_t,std::uint32_t,
|
||||
const VkPhysicalDeviceMemoryProperties&);
|
||||
void Destroy(Eye&);
|
||||
VkDevice device_{};
|
||||
PFN_vkGetDeviceProcAddr proc_{};
|
||||
std::array<Eye,2> eyes_{};
|
||||
std::array<Motion,2> motion_{};
|
||||
struct Pending {std::array<std::uint64_t,2> depth{},motion{};bool submitted=false;};
|
||||
std::unordered_map<VkCommandBuffer,Pending> pending_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// A scene-pass candidate, not an asynchronous borrow. This is delivered while
|
||||
// the guest command buffer is recording, immediately after its render pass.
|
||||
// The backend must verify owner/eye/generation, then record any required copies
|
||||
// on that command buffer before returning. Image handles may be reused by the
|
||||
// very next guest pass. A later queue submission/fence establishes completion.
|
||||
struct TemporalSceneInputs {
|
||||
std::uint32_t version = 1;
|
||||
std::uint32_t size = sizeof(TemporalSceneInputs);
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkImage color = VK_NULL_HANDLE;
|
||||
VkImage depth = VK_NULL_HANDLE;
|
||||
VkFormat color_format = VK_FORMAT_UNDEFINED;
|
||||
VkFormat depth_format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout color_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImageLayout depth_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImageSubresourceRange color_range{};
|
||||
VkImageSubresourceRange depth_range{};
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
// Set only when this pass actually clears its depth attachment. This is
|
||||
// evidence for normal (clear=1) versus reversed (clear=0) device depth.
|
||||
float depth_clear = 0.f;
|
||||
bool depth_clear_valid = false;
|
||||
void* owner = nullptr;
|
||||
void* guest_framebuffer = nullptr;
|
||||
int eye = -1;
|
||||
// Zero means the adapter has no frame number. The backend must stamp the
|
||||
// current published frame after matching the exact live eye provenance.
|
||||
std::uint64_t frame = 0;
|
||||
};
|
||||
|
||||
// Native 3222 motion shader output before conversion: current clip.xyz/w minus
|
||||
// previous clip.xyz/w. Its vertex shader negates Y only for gl_Position, so
|
||||
// previous-UV minus current-UV is nativeDelta.xy * (-0.5, +0.5). XYZ are signed
|
||||
// floats; W is zero, not a validity mask. The shader can discard large deltas.
|
||||
struct TemporalMotionInputs {
|
||||
std::uint32_t version = 1;
|
||||
std::uint32_t size = sizeof(TemporalMotionInputs);
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
VkImageSubresourceRange range{};
|
||||
std::uint32_t width = 0, height = 0;
|
||||
void* owner = nullptr;
|
||||
void* guest_framebuffer = nullptr;
|
||||
int eye = -1;
|
||||
std::uint64_t frame = 0;
|
||||
float previous_uv_scale_x = -.5f;
|
||||
float previous_uv_scale_y = .5f;
|
||||
};
|
||||
|
||||
inline bool IsTemporalMotionFormat(VkFormat format) {
|
||||
return format == VK_FORMAT_R16G16_SFLOAT ||
|
||||
format == VK_FORMAT_R16G16B16A16_SFLOAT ||
|
||||
format == VK_FORMAT_R32G32_SFLOAT ||
|
||||
format == VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
}
|
||||
|
||||
inline bool IsTemporalClipMotionShaders(std::uint64_t vertex,
|
||||
std::uint64_t fragment) {
|
||||
// FNV-1a of exact native shader module bytes, verified against both the
|
||||
// fragment subtraction and vertex current/previous clip outputs. The three
|
||||
// adjacent world-space variants deliberately have a different contract.
|
||||
return fragment == 0x821fff499ec5bdbbull &&
|
||||
(vertex == 0x9b0d6a7041cad338ull ||
|
||||
vertex == 0x4a99a561ef9805b6ull ||
|
||||
vertex == 0x6dcaa6712cd5a2cdull);
|
||||
}
|
||||
|
||||
inline bool IsTemporalDepthFormat(VkFormat format) {
|
||||
return format == VK_FORMAT_D16_UNORM ||
|
||||
format == VK_FORMAT_X8_D24_UNORM_PACK32 ||
|
||||
format == VK_FORMAT_D32_SFLOAT ||
|
||||
format == VK_FORMAT_D16_UNORM_S8_UINT ||
|
||||
format == VK_FORMAT_D24_UNORM_S8_UINT ||
|
||||
format == VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
}
|
||||
|
||||
inline bool IsTemporalColorFormat(VkFormat format) {
|
||||
return format == VK_FORMAT_R8G8B8A8_UNORM ||
|
||||
format == VK_FORMAT_R8G8B8A8_SRGB ||
|
||||
format == VK_FORMAT_B8G8R8A8_UNORM ||
|
||||
format == VK_FORMAT_B8G8R8A8_SRGB ||
|
||||
format == VK_FORMAT_R16G16B16A16_SFLOAT ||
|
||||
format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 ||
|
||||
format == VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
}
|
||||
|
||||
inline bool IsTemporalImageCandidate(const VkImageCreateInfo& info) {
|
||||
const bool depth = IsTemporalDepthFormat(info.format) &&
|
||||
(info.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
|
||||
const bool color = IsTemporalColorFormat(info.format) &&
|
||||
(info.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
|
||||
const bool motion = IsTemporalMotionFormat(info.format) &&
|
||||
(info.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
|
||||
// Native transient targets may request lazy memory; changing their usage would
|
||||
// disagree with the engine allocator's original memory-type request.
|
||||
return (depth || color || motion) &&
|
||||
!(info.usage & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) &&
|
||||
info.imageType == VK_IMAGE_TYPE_2D &&
|
||||
info.tiling == VK_IMAGE_TILING_OPTIMAL &&
|
||||
info.samples == VK_SAMPLE_COUNT_1_BIT && info.extent.depth == 1 &&
|
||||
info.extent.width >= 256 && info.extent.height >= 256 &&
|
||||
info.arrayLayers == 1 && info.mipLevels == 1 && info.flags == 0;
|
||||
}
|
||||
|
||||
// Depth/stencil formats use depth only. FSR samples device depth; stencil must
|
||||
// never be reinterpreted as scene depth or included in a depth-only copy.
|
||||
inline VkImageAspectFlags TemporalDepthAspect(VkFormat format) {
|
||||
return IsTemporalDepthFormat(format) ? VK_IMAGE_ASPECT_DEPTH_BIT : 0;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
|
||||
extern "C" {
|
||||
// Availability tracks compatible resources from Vulkan startup. Enabled only
|
||||
// selects preserving passes and per-frame capture while temporal FSR is active.
|
||||
bool mocktail_vr_xr_fsr3_inputs_available();
|
||||
bool mocktail_vr_xr_fsr3_inputs_enabled();
|
||||
void mocktail_vr_xr_fsr3_scene_end(
|
||||
VkDevice device, VkCommandBuffer command,
|
||||
const mocktail::vr::TemporalSceneInputs* inputs);
|
||||
void mocktail_vr_xr_fsr3_motion_end(
|
||||
VkDevice device, VkCommandBuffer command,
|
||||
const mocktail::vr::TemporalMotionInputs* inputs);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "mocktail/vr/fsr3_upscaler.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
// Explicit encoded-native -> linear-FSR -> encoded-native transport.
|
||||
// The caller drains uses before resizing/resetting these reusable eye images.
|
||||
class Fsr3Transport {
|
||||
public:
|
||||
~Fsr3Transport() { Reset(); }
|
||||
bool Prepare(VkDevice,PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&,VkFormat source,
|
||||
std::uint32_t width,std::uint32_t height,
|
||||
std::uint32_t output_width,std::uint32_t output_height);
|
||||
void Begin(VkCommandBuffer,int eye,VkImage encoded_source);
|
||||
void Finish(VkCommandBuffer,int eye);
|
||||
Fsr3Upscaler::Image color(int eye) const;
|
||||
Fsr3Upscaler::Image output(int eye) const;
|
||||
VkImage encoded(int eye) const {return eyes_[eye].encoded.image;}
|
||||
VkFormat encoded_format() const {return srgb_format_;}
|
||||
void Reset();
|
||||
private:
|
||||
struct Resource {VkImage image{};VkDeviceMemory memory{};};
|
||||
struct Eye {Resource color,output,encoded;};
|
||||
template<class T>T Proc(const char* name)const{return reinterpret_cast<T>(proc_(device_,name));}
|
||||
bool Create(Resource&,VkFormat,std::uint32_t,std::uint32_t,VkImageUsageFlags,
|
||||
const VkPhysicalDeviceMemoryProperties&);
|
||||
void Destroy(Resource&);
|
||||
void Barrier(VkCommandBuffer,VkImage,VkImageLayout,VkImageLayout,VkAccessFlags,VkAccessFlags);
|
||||
VkDevice device_{};
|
||||
PFN_vkGetDeviceProcAddr proc_{};
|
||||
VkFormat source_format_{},srgb_format_{};
|
||||
std::uint32_t width_{},height_{},output_width_{},output_height_{};
|
||||
std::array<Eye,2> eyes_{};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// AMD FSR 3.1.4 temporal upscaling. Each eye owns independent temporal history.
|
||||
// GPU completion is the owner's responsibility before Reset or destruction.
|
||||
class Fsr3Upscaler {
|
||||
public:
|
||||
struct Configuration {
|
||||
std::uint32_t render_width{}, render_height{};
|
||||
std::uint32_t output_width{}, output_height{};
|
||||
bool inverted_depth=false, infinite_depth=false, hdr=false;
|
||||
// Estimate image motion with AMD Optical Flow when engine vectors are
|
||||
// unavailable. This is a fallback with its own compute cost and limits.
|
||||
bool estimate_motion=false;
|
||||
bool motion_vectors_jittered=false;
|
||||
// Enable only when shaderFloat16 was enabled on the logical VkDevice.
|
||||
bool allow_fp16=false;
|
||||
};
|
||||
struct Image {
|
||||
VkImage image{};
|
||||
VkFormat format{};
|
||||
std::uint32_t width{}, height{};
|
||||
};
|
||||
struct JitterOffset { float x{}, y{}; };
|
||||
struct Frame {
|
||||
// Inputs must be sampled images in SHADER_READ_ONLY_OPTIMAL, output a
|
||||
// storage image in GENERAL. The SDK restores those layouts after dispatch.
|
||||
// Color is LINEAR: an sRGB image format decodes encoded source bytes.
|
||||
// The output should be RGBA16F linear; encode it before copying to an
|
||||
// encoded UNORM eye image. Depth and color must describe the same view.
|
||||
Image color, depth, motion_vectors, output;
|
||||
Image reactive, transparency;
|
||||
// Unjittered motion is previous pixel position minus current position.
|
||||
// Pixel-unit vectors use {1,1}; normalized UV vectors use render size.
|
||||
float motion_vector_scale[2]{1.f,1.f};
|
||||
JitterOffset jitter;
|
||||
float delta_ms=16.667f, near_plane=.1f, far_plane=1000.f;
|
||||
float vertical_fov=1.5707963f, meters_per_world_unit=1.f;
|
||||
float pre_exposure=1.f, sharpness=.2f;
|
||||
bool sharpening=true, reset=false;
|
||||
};
|
||||
|
||||
Fsr3Upscaler();
|
||||
~Fsr3Upscaler();
|
||||
Fsr3Upscaler(const Fsr3Upscaler&)=delete;
|
||||
Fsr3Upscaler& operator=(const Fsr3Upscaler&)=delete;
|
||||
bool Configure(VkPhysicalDevice, VkDevice, PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&, const Configuration&);
|
||||
bool Record(VkCommandBuffer, int eye, const Frame&);
|
||||
Image estimated_motion(int eye) const;
|
||||
void Reset();
|
||||
int last_error() const;
|
||||
static JitterOffset Jitter(std::uint64_t frame, std::uint32_t render_width,
|
||||
std::uint32_t output_width);
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::vr {
|
||||
struct FsrSettings {
|
||||
int quality=0; // Custom scale, Ultra Quality, Quality, Balanced, Performance, Custom resolution.
|
||||
int scale_percent=80;
|
||||
std::uint32_t input_resolution=1728;
|
||||
bool operator==(const FsrSettings& other) const {
|
||||
return quality==other.quality && scale_percent==other.scale_percent &&
|
||||
input_resolution==other.input_resolution;
|
||||
}
|
||||
bool operator!=(const FsrSettings& other) const { return !(*this==other); }
|
||||
};
|
||||
|
||||
inline std::uint32_t FsrRenderSize(std::uint32_t output, int mode,
|
||||
const FsrSettings& settings={}) {
|
||||
if((mode!=1 && mode!=2 && mode!=3) || output<2)return output;
|
||||
if(settings.quality==5)
|
||||
return std::clamp(settings.input_resolution,2u,output)&~1u;
|
||||
std::uint64_t numerator=std::clamp(settings.scale_percent,25,100),denominator=100;
|
||||
switch(settings.quality) {
|
||||
case 1:numerator=10;denominator=13;break;
|
||||
case 2:numerator=2;denominator=3;break;
|
||||
case 3:numerator=10;denominator=17;break;
|
||||
case 4:numerator=1;denominator=2;break;
|
||||
default:break;
|
||||
}
|
||||
const auto scaled=static_cast<std::uint32_t>(
|
||||
2*((std::uint64_t(output)*numerator+denominator)/(2*denominator)));
|
||||
return std::clamp(scaled,2u,output&~1u);
|
||||
}
|
||||
inline std::uint32_t FsrRenderHeight(std::uint32_t output_width,
|
||||
std::uint32_t output_height,int mode,
|
||||
const FsrSettings& settings={}) {
|
||||
if(settings.quality!=5 || mode==0 || !output_width || output_height<2)
|
||||
return FsrRenderSize(output_height,mode,settings);
|
||||
const auto width=FsrRenderSize(output_width,mode,settings);
|
||||
const auto height=static_cast<std::uint32_t>(
|
||||
2*((std::uint64_t(output_height)*width+output_width)/(2*std::uint64_t(output_width))));
|
||||
return std::clamp(height,2u,output_height&~1u);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "mocktail/vr/fsr_settings.h"
|
||||
#include "mocktail/vr/temporal_reprojection.h"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mocktail::vr {
|
||||
// FP32 FSR 1 EASU, followed by gaze-only RCAS after the normal gaze AA.
|
||||
// Uses raw host dispatch and the backend's existing copy command/fence.
|
||||
class FsrUpscaler {
|
||||
public:
|
||||
~FsrUpscaler() { Reset(); }
|
||||
bool Prepare(VkDevice, PFN_vkGetDeviceProcAddr,
|
||||
const VkPhysicalDeviceMemoryProperties&, int eye,
|
||||
VkImage source, VkFormat source_format, VkImage destination,
|
||||
VkFormat output_format, std::uint32_t input_width,
|
||||
std::uint32_t input_height, std::uint32_t output_width,
|
||||
std::uint32_t output_height, bool temporal=false);
|
||||
void RecordUpscale(VkCommandBuffer, int eye, float gaze_x=0, float gaze_y=0,
|
||||
float radius=.15f);
|
||||
void RecordTemporal(VkCommandBuffer,int eye,const TemporalEyePose&,
|
||||
float gaze_x,float gaze_y,float radius,std::uint64_t now);
|
||||
void RecordSharpen(VkCommandBuffer, int eye, float gaze_x, float gaze_y,
|
||||
float radius);
|
||||
VkImage image(int eye) const { return eyes_[eye].image; }
|
||||
VkFormat intermediate_format() const;
|
||||
void ForgetImage(VkImage);
|
||||
void Reset();
|
||||
private:
|
||||
template<class T> T Proc(const char* name) const {
|
||||
return reinterpret_cast<T>(proc_(device_, name));
|
||||
}
|
||||
bool Initialize(VkDevice, PFN_vkGetDeviceProcAddr, VkFormat);
|
||||
VkPipeline Pipeline(VkRenderPass, const std::uint32_t*, std::size_t);
|
||||
struct Eye {
|
||||
VkImage image{}, source{}, destination{}, gamma_image{};
|
||||
VkDeviceMemory memory{}, gamma_memory{};
|
||||
VkImageView view{}, source_view{};
|
||||
VkFramebuffer framebuffer{};
|
||||
VkDescriptorSet sets[3]{};
|
||||
VkImage history_image{};VkDeviceMemory history_memory{};VkImageView history_view{};
|
||||
bool history_initialized=false,history_failed=false;
|
||||
TemporalEyePose previous_pose;
|
||||
float previous_center[2]{},previous_radius=0;
|
||||
std::uint64_t previous_time=0;
|
||||
VkFormat source_format{};
|
||||
std::uint32_t in_width=0, in_height=0, width=0, height=0;
|
||||
};
|
||||
struct Target { VkImageView view{}; VkFramebuffer framebuffer{}; };
|
||||
void FreeEye(Eye&);
|
||||
bool PrepareHistory(const VkPhysicalDeviceMemoryProperties&,int eye);
|
||||
struct alignas(16) Constants {
|
||||
std::uint32_t con[4][4]{};
|
||||
float texel[2]{}, center[2]{};
|
||||
float radius=0;
|
||||
std::uint32_t input_srgb=0, output_srgb=0, reserved=0;
|
||||
};
|
||||
static_assert(sizeof(Constants)==96);
|
||||
struct alignas(16) TemporalConstants {
|
||||
float rows[3][4]{},texel[2]{},center[2]{},previous_center[2]{};
|
||||
float radius=0,previous_radius=0,blend=0;
|
||||
std::uint32_t output_srgb=0,reserved[2]{};
|
||||
};
|
||||
static_assert(sizeof(TemporalConstants)==96);
|
||||
void Draw(VkCommandBuffer, VkRenderPass, VkFramebuffer, VkPipeline,
|
||||
VkDescriptorSet, const void*, const Eye&, VkRect2D);
|
||||
void Barrier(VkCommandBuffer, VkImage, VkImageLayout, VkImageLayout,
|
||||
VkAccessFlags, VkAccessFlags, VkPipelineStageFlags,
|
||||
VkPipelineStageFlags);
|
||||
VkDevice device_{};
|
||||
PFN_vkGetDeviceProcAddr proc_{};
|
||||
VkFormat format_{};
|
||||
VkRenderPass passes_[2]{};
|
||||
VkPipeline pipelines_[3]{};
|
||||
VkDescriptorSetLayout descriptor_layout_{};
|
||||
VkDescriptorPool descriptor_pool_{};
|
||||
VkPipelineLayout layout_{};
|
||||
VkSampler sampler_{};
|
||||
Eye eyes_[2];
|
||||
std::unordered_map<VkImage,Target> targets_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// Gaze-limited edge filtering after the normal eye copy.
|
||||
// Every call uses raw host dispatch. The owner waits its copy fence before
|
||||
// changing descriptors, dropping views, or destroying resources.
|
||||
class GazeAa {
|
||||
public:
|
||||
~GazeAa() { Reset(); }
|
||||
bool Prepare(VkDevice, PFN_vkGetDeviceProcAddr, int eye, VkImage source,
|
||||
VkFormat source_format, VkImage destination,
|
||||
VkFormat destination_format, std::uint32_t width,
|
||||
std::uint32_t height, int mode=1,
|
||||
const VkPhysicalDeviceMemoryProperties* memory=nullptr);
|
||||
bool Record(VkCommandBuffer, int eye, float gaze_x, float gaze_y, int mode=1,
|
||||
float radius=.22f);
|
||||
void ForgetImage(VkImage);
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
template<class T> T Proc(const char* name) const {
|
||||
return reinterpret_cast<T>(get_proc_(device_,name));
|
||||
}
|
||||
bool Initialize(VkDevice,PFN_vkGetDeviceProcAddr,VkFormat);
|
||||
VkPipeline MakePipeline(VkRenderPass,const std::uint32_t*,std::size_t);
|
||||
bool PrepareSmaa(int,const VkPhysicalDeviceMemoryProperties&,int mode);
|
||||
void RecordSmaa(VkCommandBuffer,int,int,const void*,const VkRect2D&);
|
||||
void RecordPass(VkCommandBuffer,int,VkPipeline,VkRenderPass,VkFramebuffer,
|
||||
VkDescriptorSet,const void*,const VkRect2D&,bool);
|
||||
void DestroySmaa();
|
||||
struct Texture { VkImage image{}; VkDeviceMemory memory{}; VkImageView view{}; };
|
||||
bool MakeTexture(Texture&,VkFormat,unsigned,unsigned,VkImageUsageFlags);
|
||||
void FreeTexture(Texture&);
|
||||
struct SmaaEye {
|
||||
Texture textures[2];
|
||||
VkFramebuffer framebuffers[2]{};
|
||||
VkDescriptorSet descriptors[2]{};
|
||||
std::uint32_t width=0,height=0;
|
||||
VkImageView descriptor_source{};
|
||||
};
|
||||
struct Source { VkImage image{}; VkImageView view{}; VkFormat format{}; bool descriptor_ready=false; };
|
||||
struct Target { VkImageView view{}; VkFramebuffer framebuffer{}; };
|
||||
VkDevice device_{};
|
||||
PFN_vkGetDeviceProcAddr get_proc_{};
|
||||
VkFormat format_=VK_FORMAT_UNDEFINED;
|
||||
VkDescriptorSetLayout descriptor_layout_{};
|
||||
VkDescriptorPool descriptor_pool_{};
|
||||
VkDescriptorSet descriptor_sets_[2]{};
|
||||
VkPipelineLayout pipeline_layout_{};
|
||||
VkPipeline pipeline_{};
|
||||
VkRenderPass render_pass_{};
|
||||
VkSampler sampler_{};
|
||||
VkRenderPass smaa_pass_{};
|
||||
VkPipeline smaa_pipelines_[2][3]{};
|
||||
SmaaEye smaa_eyes_[2];
|
||||
Texture smaa_luts_[2];
|
||||
VkBuffer smaa_staging_{};
|
||||
VkDeviceMemory smaa_staging_memory_{};
|
||||
bool smaa_uploaded_=false;
|
||||
Source sources_[2];
|
||||
std::unordered_map<VkImage,Target> targets_;
|
||||
VkImage destinations_[2]{};
|
||||
std::uint32_t widths_[2]{},heights_[2]{};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
struct NativeProjectionSample {
|
||||
// The actual engine camera projection before temporal jitter, row-major.
|
||||
// This is read from the native camera rather than rebuilt from tracking FOV.
|
||||
float projection[16] = {};
|
||||
// The full world-view-projection used by the engine, including player
|
||||
// locomotion. Supplied by the separately verified native combined getter.
|
||||
float view_projection[16] = {};
|
||||
float jitter_pixels[2] = {};
|
||||
std::uint32_t render_width = 0;
|
||||
std::uint32_t render_height = 0;
|
||||
float camera_near = 0.f;
|
||||
float camera_far = 0.f;
|
||||
std::uintptr_t camera_identity = 0;
|
||||
std::uint64_t frame = 0;
|
||||
bool depth_inverted = false;
|
||||
bool depth_infinite = false;
|
||||
bool jitter_applied = false;
|
||||
bool view_projection_valid = false;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// True only when the exact native per-eye projection function was verified and
|
||||
// hooked. Unsupported clients retain their original projections.
|
||||
bool NativeProjectionBridgeReady();
|
||||
// Copies only a projection from the requested frame. A stale or unsupported
|
||||
// camera never supplies temporal reconstruction parameters.
|
||||
bool ReadNativeProjectionSample(int eye, std::uint64_t expected_frame,
|
||||
NativeProjectionSample* sample);
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -8,12 +8,21 @@
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "mocktail/vr/xr_foveation_maps.h"
|
||||
#include "mocktail/vr/gaze_aa.h"
|
||||
#include "mocktail/vr/fsr_upscaler.h"
|
||||
#include "mocktail/vr/fsr3_scene_capture.h"
|
||||
#ifdef MOCKTAIL_ENABLE_FSR3
|
||||
#include "mocktail/vr/fsr3_upscaler.h"
|
||||
#include "mocktail/vr/fsr3_gaze_transport.h"
|
||||
#endif
|
||||
#include "mocktail/vr/virtual_keyboard.h"
|
||||
#include "mocktail/vr/vr_mirror.h"
|
||||
#include "mocktail/vr/xr_actions.h"
|
||||
@@ -22,6 +31,12 @@
|
||||
namespace mocktail::vr {
|
||||
|
||||
namespace internal {
|
||||
// Deferral is safe only when the guest requested exactly one unprotected queue.
|
||||
bool SingleUnprotectedVulkanQueue(const VkDeviceCreateInfo* info);
|
||||
// Frame's native Video slider stores a per-eye pixel dimension per app.
|
||||
std::uint32_t SteamFrameEyeResolution(std::string_view settings,
|
||||
std::string_view steam_app_id);
|
||||
|
||||
// Swapchain format for guest eyes of `eye` given the runtime's formats:
|
||||
// `eye` itself, else its UNORM/SRGB sibling (a raw byte copy, since the guest
|
||||
// writes sRGB-encoded bytes either way), else the other channel order with
|
||||
@@ -89,6 +104,9 @@ struct ScriptedPoseSample {
|
||||
float orientation[4] = {0.f, 0.f, 0.f, 1.f};
|
||||
float eye_offset[2][3] = {}; // metres in head space
|
||||
float eye_fov[2][4] = {}; // left, right, up, down angles
|
||||
float projection_jitter_pixels[2] = {}; // raster displacement: right/down
|
||||
std::uint32_t projection_render_width = 0, projection_render_height = 0;
|
||||
bool projection_jitter_valid = false;
|
||||
// Index 0 = left, 1 = right (Roblox UserCFrame LeftHand/RightHand).
|
||||
VrHandPose hands[2];
|
||||
float floor_position[3] = {};
|
||||
@@ -160,8 +178,25 @@ class OpenXrBackend final {
|
||||
const VkDeviceCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkDevice* device, VkResult* result);
|
||||
void NoteDeviceDestroyed(VkDevice device);
|
||||
void NoteInstanceDestroyed(VkInstance instance);
|
||||
// True transfers native device destruction to this backend.
|
||||
bool NoteDeviceDestroyed(VkDevice device);
|
||||
bool FoveationEnabled(VkDevice device) const;
|
||||
VkImageView FoveationViewForFramebuffer(VkDevice device, VkFramebuffer framebuffer);
|
||||
bool FoveationFocus(VkDevice device,VkFramebuffer framebuffer,float* x,float* y,std::uint64_t* frame,int* eye,int* level);
|
||||
bool FoveationSnapshot(VkDevice device,float* centers,float* radius,std::uint64_t* frame,int* level);
|
||||
// A scene intermediate may be shared between eyes. Resolve it from the live
|
||||
// eye getter, never from its framebuffer handle or dimensions alone.
|
||||
bool FoveationRenderFocus(VkDevice device, VkFramebuffer framebuffer,
|
||||
std::uint32_t scene_width, std::uint32_t scene_height, float* x, float* y,
|
||||
float* radius, std::uint64_t* frame, int* eye, int* level);
|
||||
bool FoveationHostCopyEnabled(VkDevice device) const;
|
||||
bool Fsr3InputsAvailable() const;
|
||||
bool Fsr3InputsEnabled() const;
|
||||
void CaptureFsr3Scene(VkDevice, VkCommandBuffer, const TemporalSceneInputs&);
|
||||
void CaptureFsr3Motion(VkDevice, VkCommandBuffer, const TemporalMotionInputs&);
|
||||
VkExtent2D FoveationMapExtent(VkDevice device) const;
|
||||
// True transfers native instance destruction to this backend.
|
||||
bool NoteInstanceDestroyed(VkInstance instance);
|
||||
void NoteQueue(VkDevice device, VkQueue queue, std::uint32_t family,
|
||||
std::uint32_t index);
|
||||
std::vector<std::uint32_t> DesktopQueueFamilies(VkDevice device) const;
|
||||
@@ -183,6 +218,11 @@ class OpenXrBackend final {
|
||||
VkImageLayout* old_layout,
|
||||
VkImageLayout new_layout);
|
||||
|
||||
void NormalizeDesktopBarriers(VkCommandBuffer command, std::uint32_t count,
|
||||
VkImageMemoryBarrier* barriers);
|
||||
void NormalizeDesktopBarriers(VkCommandBuffer command, std::uint32_t count,
|
||||
VkImageMemoryBarrier2* barriers);
|
||||
|
||||
// Resource provenance recording (adapter wrappers).
|
||||
bool WantsResourceRecords() const {
|
||||
return recording_.load(std::memory_order_acquire);
|
||||
@@ -228,6 +268,9 @@ class OpenXrBackend final {
|
||||
// poses (metres, LOCAL space) when a controller layer is active.
|
||||
ScriptedPoseSample PublishedHeadPose() const;
|
||||
bool RecommendedEyeExtent(std::uint32_t* width, std::uint32_t* height) const;
|
||||
// Render-thread boundary only: rebuild XR targets when Steam's settled
|
||||
// resolution changes. The guest then reruns its verified eye initializer.
|
||||
bool UpdateEyeResolution();
|
||||
void NotePoseApplied(void* owner, std::uint64_t frame);
|
||||
|
||||
// Controller input delivery, consumed by the window-thread input runtime
|
||||
@@ -255,6 +298,11 @@ class OpenXrBackend final {
|
||||
std::string runtime_name() const;
|
||||
|
||||
private:
|
||||
VkImageLayout NormalizeDesktopBarrierLocked(std::uint32_t family,
|
||||
VkImage image, std::uint32_t* source, std::uint32_t* destination,
|
||||
VkImageLayout* old_layout, VkImageLayout new_layout);
|
||||
bool FoveationSnapshotLocked(VkDevice,float*,float*,std::uint64_t*,int*);
|
||||
int FoveationSceneEyeLocked(VkDevice, std::uint32_t, std::uint32_t) const;
|
||||
friend struct OpenXrBackendTestAccess;
|
||||
struct ImageRecord {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
@@ -272,6 +320,7 @@ class OpenXrBackend final {
|
||||
std::uint64_t frame = 0;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
void* guest_framebuffer = nullptr;
|
||||
VkImageLayout final_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
void* owner = nullptr;
|
||||
std::uint32_t width = 0;
|
||||
@@ -323,6 +372,7 @@ class OpenXrBackend final {
|
||||
bool RuntimeUsesGuestGpuLocked();
|
||||
void PollSessionEvents();
|
||||
bool SupportsPresentQueueLocked(VkQueue queue) const;
|
||||
VkQueue FrameQueueForPresentLocked(VkQueue queue) const;
|
||||
VkQueue GraphicsQueueForPresentLocked(VkQueue present) const;
|
||||
bool unsupported_present_queue_logged_ = false;
|
||||
// Drops the pose/images of the frame currently in flight without touching
|
||||
@@ -333,11 +383,13 @@ class OpenXrBackend final {
|
||||
// Returns true when the backend owns this present but ran no XR frame, so
|
||||
// the caller paces the guest itself.
|
||||
bool NoteHostPresentLocked(VkQueue queue, VkDevice device);
|
||||
bool NeedsIdlePacingLocked() const;
|
||||
bool CreateSwapchains(std::string* error);
|
||||
// Recreates the eye swapchains once in the format that copies the guest's
|
||||
// eye bytes without a gamma change (see PreferredEyeSwapchainFormat).
|
||||
void MatchSwapchainFormatToEyesLocked();
|
||||
void DestroySwapchains();
|
||||
void ResetFsr3Locked();
|
||||
bool EnsureCopyResources(VkDevice device, std::uint32_t queue_family,
|
||||
std::string* error);
|
||||
void DestroyCopyResources();
|
||||
@@ -347,7 +399,7 @@ class OpenXrBackend final {
|
||||
bool CopyEyesIntoSwapchains(VkQueue queue);
|
||||
void RecordEyeCopiesLocked(VkCommandBuffer command, const VkImage source[2],
|
||||
const VkImage destination[2]);
|
||||
void RecordFilteredEyesLocked(VkCommandBuffer command, bool smaa);
|
||||
void RecordFxaaEyesLocked(VkCommandBuffer command);
|
||||
// Per-frame GPU work (desktop mirror, eye copies) is submitted without a CPU
|
||||
// wait. A slot is reused only after its fence signalled; its images let a
|
||||
// guest destroy wait for the submissions that still read them.
|
||||
@@ -355,6 +407,7 @@ class OpenXrBackend final {
|
||||
VkCommandBuffer command = VK_NULL_HANDLE;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
bool pending = false;
|
||||
bool eye_resources = false;
|
||||
std::uint64_t serial = 0; // bumped per submission
|
||||
std::vector<VkImage> images;
|
||||
};
|
||||
@@ -392,60 +445,13 @@ class OpenXrBackend final {
|
||||
std::vector<VkFramebuffer> framebuffers[2];
|
||||
bool failed = false;
|
||||
};
|
||||
// vr.anti_aliasing. SMAA shares FXAA's final pass, targets and eye views.
|
||||
enum class AntiAliasing { kOff, kFxaa, kSmaa };
|
||||
AntiAliasing AntiAliasingMode() const;
|
||||
bool FxaaEnabled() const;
|
||||
// Pipeline, targets and eye bindings for this frame; false means copy.
|
||||
bool PrepareFxaaLocked();
|
||||
bool CreateFxaaPipelineLocked(VkFormat target_format);
|
||||
bool CreateFullscreenPipelineLocked(VkShaderModule vertex,
|
||||
VkShaderModule fragment,
|
||||
VkPipelineLayout layout,
|
||||
VkRenderPass render_pass,
|
||||
VkPipeline* pipeline);
|
||||
void DestroyFxaaTargetsLocked();
|
||||
void ForgetFxaaSourceLocked(VkImage image);
|
||||
void DestroyFxaaLocked();
|
||||
// SMAA: edge detection and blending weights into per-eye images, then
|
||||
// neighborhood blending into FXAA's swapchain targets.
|
||||
struct SmaaPass {
|
||||
VkDescriptorSetLayout set_layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout layout = VK_NULL_HANDLE;
|
||||
VkRenderPass render_pass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sets[2] = {};
|
||||
};
|
||||
struct SmaaImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
};
|
||||
struct SmaaState {
|
||||
std::vector<VkShaderModule> modules;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
SmaaPass edges, weights, blend;
|
||||
SmaaImage area, search;
|
||||
// Per eye, at the eye's extent.
|
||||
SmaaImage edge_images[2], weight_images[2];
|
||||
VkFramebuffer edge_framebuffers[2] = {};
|
||||
VkFramebuffer weight_framebuffers[2] = {};
|
||||
std::uint32_t width = 0, height = 0;
|
||||
VkFormat target_format = VK_FORMAT_UNDEFINED;
|
||||
// The eye views the edge and blend sets were written with.
|
||||
VkImageView bound_sources[2] = {};
|
||||
bool failed = false;
|
||||
};
|
||||
bool PrepareSmaaLocked();
|
||||
bool CreateSmaaPipelinesLocked();
|
||||
bool CreateSmaaImageLocked(VkFormat format, std::uint32_t width,
|
||||
std::uint32_t height, VkImageUsageFlags usage,
|
||||
SmaaImage* out);
|
||||
bool UploadSmaaLookupLocked(SmaaImage* image, std::uint32_t width,
|
||||
std::uint32_t height, const void* bytes,
|
||||
std::size_t size);
|
||||
void DestroySmaaImageLocked(SmaaImage* image);
|
||||
void DestroySmaaTargetsLocked();
|
||||
void DestroySmaaLocked();
|
||||
// The next slot, recorded-ready, or nullptr when its previous submission
|
||||
// did not finish in time (the device is hung or lost).
|
||||
AsyncSubmission* NextAsyncSubmissionLocked();
|
||||
@@ -454,6 +460,9 @@ class OpenXrBackend final {
|
||||
// Waits for pending submissions reading `image`, or all of them for
|
||||
// VK_NULL_HANDLE.
|
||||
void WaitAsyncSubmissionsLocked(VkImage image);
|
||||
// AA/FSR descriptors and history are shared between eye copies. Wait only
|
||||
// for their previous users, never the current guest-present dependency.
|
||||
void WaitEyeResourcesLocked();
|
||||
// Semaphore the present of `image_index` waits on, created on first use.
|
||||
enum class PresentStep { kMirror, kPrepare };
|
||||
VkSemaphore PresentReadySemaphoreLocked(VkSwapchainKHR swapchain,
|
||||
@@ -503,6 +512,10 @@ class OpenXrBackend final {
|
||||
void* xr_stage_space_ = nullptr;
|
||||
std::uint32_t xr_eye_width_[2] = {};
|
||||
std::uint32_t xr_eye_height_[2] = {};
|
||||
std::uint32_t xr_max_eye_width_[2] = {};
|
||||
std::uint32_t xr_max_eye_height_[2] = {};
|
||||
std::uint32_t resize_candidate_ = 0;
|
||||
std::uint64_t resize_candidate_since_ = 0;
|
||||
EyeSwapchain eye_swapchains_[2];
|
||||
VirtualKeyboard keyboard_;
|
||||
std::unique_ptr<VirtualKeyboardRasterizer> keyboard_raster_;
|
||||
@@ -531,11 +544,52 @@ class OpenXrBackend final {
|
||||
std::string runtime_manifest_;
|
||||
|
||||
VkInstance vk_instance_ = VK_NULL_HANDLE;
|
||||
std::uint32_t vk_instance_api_version_ = VK_API_VERSION_1_0;
|
||||
bool fsr3_device_supported_ = false;
|
||||
// One retained instance avoids invalid SteamVR ARM graphics-device caches
|
||||
// during menu/game transitions. Only an identical creation request reuses it.
|
||||
bool vk_instance_retained_ = false;
|
||||
bool vk_device_retained_ = false;
|
||||
bool foveation_runtime_available_ = false;
|
||||
bool eye_gaze_available_ = false;
|
||||
bool foveation_device_enabled_ = false;
|
||||
bool foveation_host_copy_enabled_ = false;
|
||||
bool foveation_used_in_frame_ = false;
|
||||
XrFoveationMaps foveation_;
|
||||
GazeAa gaze_aa_;
|
||||
FsrUpscaler fsr_;
|
||||
Fsr3SceneCapture fsr3_capture_;
|
||||
#ifdef MOCKTAIL_ENABLE_FSR3
|
||||
Fsr3Upscaler fsr3_;
|
||||
Fsr3GazeTransport fsr3_transport_;
|
||||
#endif
|
||||
std::atomic<bool> fsr3_inputs_enabled_{false};
|
||||
bool fsr3_failure_logged_ = false;
|
||||
std::uint64_t fsr3_last_frame_ns_ = 0;
|
||||
std::uint64_t fsr3_last_frame_ = 0;
|
||||
bool fsr3_has_resources_ = false;
|
||||
bool fsr3_capture_seen_[2]{};
|
||||
float fsr3_gaze_radius_ = -1.f;
|
||||
bool fsr3_contexts_configured_ = false;
|
||||
bool fsr3_estimate_motion_ = true;
|
||||
bool fsr3_depth_inverted_ = false, fsr3_depth_infinite_ = false;
|
||||
std::uint64_t fsr3_camera_identity_[2]{};
|
||||
float fsr3_camera_near_[2]{}, fsr3_camera_far_[2]{};
|
||||
static constexpr int upscale_mode_ = 0;
|
||||
FsrSettings fsr_settings_{};
|
||||
bool fsr_failure_logged_ = false;
|
||||
std::string guest_device_key_;
|
||||
static std::string VulkanDeviceKey(const VkDeviceCreateInfo* info);
|
||||
void ReleaseGuestRendererLocked();
|
||||
unsigned guest_device_generation_ = 0;
|
||||
bool defer_initial_session_ = false;
|
||||
std::string guest_instance_key_;
|
||||
VkDevice vk_device_ = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice vk_physical_device_ = VK_NULL_HANDLE;
|
||||
std::uint32_t vk_queue_family_ = 0;
|
||||
VkQueue vk_graphics_queue_ = VK_NULL_HANDLE;
|
||||
std::vector<std::uint32_t> device_queue_families_;
|
||||
bool single_guest_queue_ = false;
|
||||
void* vk_loader_ = nullptr;
|
||||
struct VkProcs;
|
||||
VkProcs* vk_ = nullptr;
|
||||
@@ -550,7 +604,6 @@ class OpenXrBackend final {
|
||||
std::size_t next_async_ = 0;
|
||||
MirrorReadback mirror_readback_{};
|
||||
FxaaState fxaa_{};
|
||||
SmaaState smaa_{};
|
||||
// The mirror's signal for this present, which PrepareHostPresent passes on.
|
||||
VkSemaphore mirror_ready_ = VK_NULL_HANDLE;
|
||||
// Semaphores of destroyed desktop swapchains. A present may still wait on
|
||||
@@ -631,6 +684,8 @@ class OpenXrBackend final {
|
||||
bool projection_rejection_logged_ = false;
|
||||
mutable ScriptedPoseSample published_pose_;
|
||||
std::uint64_t xr_frame_counter_ = 0;
|
||||
// All begun XR frames, including an unworn/invisible headset.
|
||||
std::uint64_t xr_begun_frames_ = 0;
|
||||
std::unordered_map<void*, std::uint64_t> applied_poses_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef MOCKTAIL_VR_PERFORMANCE_STATS_H_
|
||||
#define MOCKTAIL_VR_PERFORMANCE_STATS_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
struct PerformanceSnapshot {
|
||||
std::uint64_t timestamp_ns = 0;
|
||||
std::uint64_t frames = 0;
|
||||
std::uint64_t last_frame_ns = 0;
|
||||
std::uint64_t process = 0;
|
||||
std::uint64_t width = 0, height = 0;
|
||||
bool visible = false;
|
||||
std::uint64_t render_width = 0, render_height = 0;
|
||||
};
|
||||
|
||||
// Count successful application projections over a rolling second. Neither
|
||||
// the scheduler's cap nor compositor repeats count as new game frames.
|
||||
class PerformanceRate {
|
||||
public:
|
||||
void Reset() { samples_.clear(); }
|
||||
double Sample(std::uint64_t now, const PerformanceSnapshot& snapshot) {
|
||||
if (!snapshot.visible || snapshot.timestamp_ns > now ||
|
||||
now - snapshot.timestamp_ns > kSecond ||
|
||||
(!samples_.empty() && (now <= samples_.back().time ||
|
||||
snapshot.frames < samples_.back().frames))) {
|
||||
Reset();
|
||||
return -1.;
|
||||
}
|
||||
samples_.push_back({now, snapshot.frames});
|
||||
const auto cutoff = now > kSecond ? now - kSecond : 0;
|
||||
while (samples_.size() > 2 && samples_[1].time <= cutoff)
|
||||
samples_.pop_front();
|
||||
if (!snapshot.last_frame_ns || snapshot.last_frame_ns > now ||
|
||||
now - snapshot.last_frame_ns >= kSecond) return 0.;
|
||||
const auto elapsed = now - samples_.front().time;
|
||||
if (elapsed < kSecond / 2) return -1.;
|
||||
return double(snapshot.frames - samples_.front().frames) * 1.e9 /
|
||||
double(elapsed);
|
||||
}
|
||||
private:
|
||||
static constexpr std::uint64_t kSecond = 1'000'000'000;
|
||||
struct SamplePoint { std::uint64_t time, frames; };
|
||||
std::deque<SamplePoint> samples_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::vr
|
||||
#endif
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef MOCKTAIL_VR_PERFORMANCE_TELEMETRY_H_
|
||||
#define MOCKTAIL_VR_PERFORMANCE_TELEMETRY_H_
|
||||
|
||||
#include "mocktail/vr/performance_stats.h"
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// The launcher owns a private tmpfs file for the lifetime of one application.
|
||||
// Only lock-free stores run in the frame path: no file writes, GPU waits or
|
||||
// frame history allocations. A separate noninteractive overlay reads it.
|
||||
struct alignas(64) SharedPerformanceStats {
|
||||
static constexpr std::uint64_t kMagic = 0x4e43504600000002ULL;
|
||||
std::atomic<std::uint64_t> magic{0}, sequence{0};
|
||||
std::atomic<std::uint64_t> timestamp_ns{0}, frames{0}, last_frame_ns{0};
|
||||
std::atomic<std::uint64_t> process{0}, width{0}, height{0}, visible{0};
|
||||
std::atomic<std::uint64_t> render_width{0}, render_height{0};
|
||||
};
|
||||
static_assert(std::atomic<std::uint64_t>::is_always_lock_free);
|
||||
|
||||
class PerformanceMapping {
|
||||
public:
|
||||
PerformanceMapping() = default;
|
||||
PerformanceMapping(const PerformanceMapping&) = delete;
|
||||
PerformanceMapping& operator=(const PerformanceMapping&) = delete;
|
||||
~PerformanceMapping() {
|
||||
if (record_) munmap(record_, sizeof(SharedPerformanceStats));
|
||||
}
|
||||
bool Open(const char* path, bool writer) {
|
||||
if (record_) return true;
|
||||
if (!path || path[0] != '/') return false;
|
||||
const int fd = open(path, (writer ? O_RDWR : O_RDONLY) | O_CLOEXEC | O_NOFOLLOW);
|
||||
if (fd < 0) return false;
|
||||
struct stat info{};
|
||||
if (fstat(fd, &info) || !S_ISREG(info.st_mode) || info.st_uid != geteuid() ||
|
||||
(info.st_mode & 0077) || info.st_size < static_cast<off_t>(sizeof(SharedPerformanceStats))) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
void* mapped = mmap(nullptr, sizeof(SharedPerformanceStats),
|
||||
writer ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (mapped == MAP_FAILED) return false;
|
||||
record_ = static_cast<SharedPerformanceStats*>(mapped);
|
||||
if (writer) {
|
||||
new (record_) SharedPerformanceStats;
|
||||
record_->magic.store(SharedPerformanceStats::kMagic, std::memory_order_release);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void Publish(const PerformanceSnapshot& value) {
|
||||
if (!record_) return;
|
||||
record_->sequence.fetch_add(1, std::memory_order_acq_rel);
|
||||
record_->timestamp_ns.store(value.timestamp_ns, std::memory_order_relaxed);
|
||||
record_->frames.store(value.frames, std::memory_order_relaxed);
|
||||
record_->last_frame_ns.store(value.last_frame_ns, std::memory_order_relaxed);
|
||||
record_->process.store(value.process, std::memory_order_relaxed);
|
||||
record_->width.store(value.width, std::memory_order_relaxed);
|
||||
record_->height.store(value.height, std::memory_order_relaxed);
|
||||
record_->visible.store(value.visible, std::memory_order_relaxed);
|
||||
record_->render_width.store(value.render_width, std::memory_order_relaxed);
|
||||
record_->render_height.store(value.render_height, std::memory_order_relaxed);
|
||||
record_->sequence.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
bool Read(PerformanceSnapshot* value) const {
|
||||
if (!record_ || record_->magic.load(std::memory_order_acquire) != SharedPerformanceStats::kMagic)
|
||||
return false;
|
||||
for (int attempt = 0; attempt < 8; ++attempt) {
|
||||
const auto sequence = record_->sequence.load(std::memory_order_acquire);
|
||||
if (sequence & 1) continue;
|
||||
PerformanceSnapshot snapshot;
|
||||
snapshot.timestamp_ns = record_->timestamp_ns.load(std::memory_order_relaxed);
|
||||
snapshot.frames = record_->frames.load(std::memory_order_relaxed);
|
||||
snapshot.last_frame_ns = record_->last_frame_ns.load(std::memory_order_relaxed);
|
||||
snapshot.process = record_->process.load(std::memory_order_relaxed);
|
||||
snapshot.width = record_->width.load(std::memory_order_relaxed);
|
||||
snapshot.height = record_->height.load(std::memory_order_relaxed);
|
||||
snapshot.visible = record_->visible.load(std::memory_order_relaxed) != 0;
|
||||
snapshot.render_width = record_->render_width.load(std::memory_order_relaxed);
|
||||
snapshot.render_height = record_->render_height.load(std::memory_order_relaxed);
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
if (sequence == record_->sequence.load(std::memory_order_relaxed)) {
|
||||
*value = snapshot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
SharedPerformanceStats* record_ = nullptr;
|
||||
};
|
||||
|
||||
class PerformancePublisher {
|
||||
public:
|
||||
void Update(std::uint64_t now, std::uint64_t frames, bool visible,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t render_width, std::uint32_t render_height) {
|
||||
if (!attempted_) {
|
||||
attempted_ = true;
|
||||
process_ = static_cast<std::uint64_t>(getpid());
|
||||
enabled_ = mapping_.Open(std::getenv("NIGHTCAP_PERFORMANCE_TELEMETRY"), true);
|
||||
}
|
||||
if (!enabled_) return;
|
||||
if (frames != previous_frames_) last_frame_ns_ = now;
|
||||
previous_frames_ = frames;
|
||||
mapping_.Publish({now, frames, last_frame_ns_, process_,
|
||||
width, height, visible, render_width, render_height});
|
||||
}
|
||||
private:
|
||||
PerformanceMapping mapping_;
|
||||
bool attempted_ = false, enabled_ = false;
|
||||
std::uint64_t previous_frames_ = 0, last_frame_ns_ = 0, process_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mocktail::vr
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
inline bool ValidRasterJitter(bool enabled, const float (&jitter_pixels)[2],
|
||||
std::uint32_t width, std::uint32_t height) {
|
||||
return enabled && width && height &&
|
||||
std::isfinite(jitter_pixels[0]) && std::isfinite(jitter_pixels[1]) &&
|
||||
std::abs(jitter_pixels[0]) <= 1.f &&
|
||||
std::abs(jitter_pixels[1]) <= 1.f;
|
||||
}
|
||||
|
||||
// Adjusts the native projection's positive {up, down, left, right} tangent
|
||||
// extents. Jitter is image displacement in render pixels: +X right, +Y down.
|
||||
// With the existing right-handed camera looking down -Z, a ray projects to
|
||||
// u=(x/-z+left)/(left+right), v=(up-y/-z)/(up+down). Translating both frustum
|
||||
// bounds preserves its angular span and displaces every ray by the same pixels.
|
||||
// Only the caller-owned projection changes; runtime FOV and tracking do not.
|
||||
inline bool ApplyRasterProjectionJitter(float (&tangents)[4], bool enabled,
|
||||
const float (&jitter_pixels)[2],
|
||||
std::uint32_t render_width,
|
||||
std::uint32_t render_height) {
|
||||
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
|
||||
return false;
|
||||
for (float value : tangents)
|
||||
if (!std::isfinite(value) || value <= 0.f)
|
||||
return false;
|
||||
|
||||
const double dx = static_cast<double>(jitter_pixels[0]) *
|
||||
(static_cast<double>(tangents[2]) + tangents[3]) /
|
||||
render_width;
|
||||
const double dy = static_cast<double>(jitter_pixels[1]) *
|
||||
(static_cast<double>(tangents[0]) + tangents[1]) /
|
||||
render_height;
|
||||
const std::array<float, 4> shifted = {
|
||||
static_cast<float>(tangents[0] + dy),
|
||||
static_cast<float>(tangents[1] - dy),
|
||||
static_cast<float>(tangents[2] + dx),
|
||||
static_cast<float>(tangents[3] - dx)};
|
||||
for (float value : shifted)
|
||||
if (!std::isfinite(value) || value <= 0.f)
|
||||
return false;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
tangents[i] = shifted[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
// The verified native Camera getter mirrors M02 for the right eye. Apply jitter
|
||||
// after that operation to give both eyes the same displacement. Vulkan image Y
|
||||
// increases downward; the verified native scene shader negates clip Y before
|
||||
// its Vulkan viewport, while camera-space Y and this projection increase up.
|
||||
inline bool ApplyRasterMatrixProjectionJitter(float (&matrix)[16], bool enabled,
|
||||
const float (&jitter_pixels)[2],
|
||||
std::uint32_t render_width,
|
||||
std::uint32_t render_height) {
|
||||
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
|
||||
return false;
|
||||
for (float value : matrix)
|
||||
if (!std::isfinite(value))
|
||||
return false;
|
||||
if (matrix[0] <= 0.f || matrix[5] <= 0.f || matrix[14] != -1.f ||
|
||||
matrix[15] != 0.f)
|
||||
return false;
|
||||
const float x = matrix[2] - 2.f * jitter_pixels[0] / render_width;
|
||||
const float y = matrix[6] + 2.f * jitter_pixels[1] / render_height;
|
||||
if (!std::isfinite(x) || !std::isfinite(y))
|
||||
return false;
|
||||
matrix[2] = x;
|
||||
matrix[6] = y;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ApplyRasterViewProjectionJitter(float (&matrix)[16], bool enabled,
|
||||
const float (&jitter_pixels)[2],
|
||||
std::uint32_t render_width,
|
||||
std::uint32_t render_height) {
|
||||
if (!ValidRasterJitter(enabled, jitter_pixels, render_width, render_height))
|
||||
return false;
|
||||
for (float value : matrix)
|
||||
if (!std::isfinite(value))
|
||||
return false;
|
||||
double denominator_norm = 0.;
|
||||
for (int i = 12; i < 16; ++i)
|
||||
denominator_norm += static_cast<double>(matrix[i]) * matrix[i];
|
||||
if (denominator_norm <= 0.)
|
||||
return false;
|
||||
const float dx = 2.f * jitter_pixels[0] / render_width;
|
||||
const float dy = -2.f * jitter_pixels[1] / render_height;
|
||||
float shifted[8];
|
||||
for (int column = 0; column < 4; ++column) {
|
||||
shifted[column] = matrix[column] + dx * matrix[12 + column];
|
||||
shifted[4 + column] = matrix[4 + column] + dy * matrix[12 + column];
|
||||
}
|
||||
for (float value : shifted)
|
||||
if (!std::isfinite(value))
|
||||
return false;
|
||||
std::copy_n(shifted, 8, matrix);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The observed native reverse-Z perspective builder writes M22=n/(f-n),
|
||||
// M23=n*f/(f-n), M32=-1. Derive its actual clip planes without assuming a
|
||||
// fixed near plane, which the client changes with its camera mode.
|
||||
inline bool DecodeReverseProjectionDepth(const float (&matrix)[16],
|
||||
float* camera_near,
|
||||
float* camera_far,
|
||||
bool* infinite) {
|
||||
if (!camera_near || !camera_far || !infinite || matrix[14] != -1.f ||
|
||||
matrix[15] != 0.f)
|
||||
return false;
|
||||
for (float value : matrix)
|
||||
if (!std::isfinite(value))
|
||||
return false;
|
||||
const double a = matrix[10], b = matrix[11];
|
||||
if (a < 0. || b <= 0.)
|
||||
return false;
|
||||
const float near = static_cast<float>(b / (a + 1.));
|
||||
const bool is_infinite = a == 0.;
|
||||
const float far = is_infinite ? std::numeric_limits<float>::infinity()
|
||||
: static_cast<float>(b / a);
|
||||
if (!std::isfinite(near) || near <= 0.f ||
|
||||
(!is_infinite && (!std::isfinite(far) || far <= near)))
|
||||
return false;
|
||||
*camera_near = near;
|
||||
*camera_far = far;
|
||||
*infinite = is_infinite;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::vr
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::vr::internal {
|
||||
struct CpuCapacity { unsigned id; unsigned capacity; };
|
||||
|
||||
// Input contains only CPUs already allowed for this thread. Keep near-peer
|
||||
// performance cores together, and retain the original mask if information is
|
||||
// incomplete or a preference would leave fewer than two available cores.
|
||||
inline std::vector<unsigned> PerformanceCpuSubset(
|
||||
const std::vector<CpuCapacity>& allowed) {
|
||||
std::vector<unsigned> original;
|
||||
unsigned maximum = 0;
|
||||
bool complete = true;
|
||||
for (const auto& cpu : allowed) {
|
||||
original.push_back(cpu.id);
|
||||
maximum = std::max(maximum, cpu.capacity);
|
||||
complete = complete && cpu.capacity > 0;
|
||||
}
|
||||
if (!complete || !maximum || allowed.size() < 2) return original;
|
||||
std::vector<unsigned> selected;
|
||||
for (const auto& cpu : allowed)
|
||||
if (std::uint64_t(cpu.capacity) * 5 >= std::uint64_t(maximum) * 4)
|
||||
selected.push_back(cpu.id);
|
||||
return selected.size() >= 2 ? selected : original;
|
||||
}
|
||||
} // namespace mocktail::vr::internal
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
#include "mocktail/status.h"
|
||||
#include "mocktail/vr/roblox_vr_guest_abi.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace internal {
|
||||
@@ -34,7 +35,7 @@ std::string DescribeSupportedVrBuilds();
|
||||
// observed presenting.
|
||||
class RobloxVrDeviceBridge final {
|
||||
public:
|
||||
using StateGetterFn = void *(*)(void *result_buffer, void *device_object);
|
||||
using StateGetterFn = guest_abi::StateGetterFn;
|
||||
using EyeGetterFn = void *(*)(void *device_object, int eye_index);
|
||||
using EyeInitializerFn = void (*)(void *device_object, void *graphics_device);
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef MOCKTAIL_VR_ROBLOX_VR_GUEST_ABI_H_
|
||||
#define MOCKTAIL_VR_ROBLOX_VR_GUEST_ABI_H_
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
namespace mocktail::vr::guest_abi {
|
||||
// AAPCS64 uses x0 for self and x8 for an indirect aggregate result. Declaring
|
||||
// the actual aggregate return lets the compiler preserve the guest ABI.
|
||||
struct StateResult { std::uint8_t bytes[0x138]; };
|
||||
struct FrameResult { float elements[12]; };
|
||||
static_assert(sizeof(StateResult) == 0x138);
|
||||
static_assert(sizeof(FrameResult) == 0x30);
|
||||
#if defined(__aarch64__)
|
||||
using StateGetterFn = StateResult (*)(void* self);
|
||||
using PointerFrameFn = FrameResult (*)(void* self);
|
||||
inline void CallState(StateGetterFn fn, void* result, void* self) {
|
||||
const auto value = fn(self);
|
||||
std::memcpy(result, &value, sizeof(value));
|
||||
}
|
||||
inline void CallPointer(PointerFrameFn fn, void* result, void* self) {
|
||||
const auto value = fn(self);
|
||||
std::memcpy(result, &value, sizeof(value));
|
||||
}
|
||||
#else
|
||||
using StateGetterFn = void* (*)(void* result, void* self);
|
||||
using PointerFrameFn = void* (*)(void* result, void* self);
|
||||
inline void CallState(StateGetterFn fn, void* result, void* self) {
|
||||
fn(result, self);
|
||||
}
|
||||
inline void CallPointer(PointerFrameFn fn, void* result, void* self) {
|
||||
fn(result, self);
|
||||
}
|
||||
#endif
|
||||
} // namespace mocktail::vr::guest_abi
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include "mocktail/vr/vr_pose_math.h"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::vr {
|
||||
struct TemporalEyePose {
|
||||
float orientation[4]={0,0,0,1};
|
||||
float fov[4]={-1,1,1,-1};
|
||||
float position[3]={};
|
||||
};
|
||||
|
||||
// Depthless rotational reprojection. Translation has no reliable depth-based
|
||||
// correspondence, so reject history during appreciable eye translation.
|
||||
inline bool TemporalReprojection(const TemporalEyePose& current,
|
||||
const TemporalEyePose& previous,
|
||||
float rows[3][4]) {
|
||||
pose::Quat c,p;
|
||||
if(!pose::QuatNormalize({current.orientation[0],current.orientation[1],current.orientation[2],current.orientation[3]},&c) ||
|
||||
!pose::QuatNormalize({previous.orientation[0],previous.orientation[1],previous.orientation[2],previous.orientation[3]},&p))return false;
|
||||
float distance=0;
|
||||
for(int i=0;i<3;++i){const float d=current.position[i]-previous.position[i];if(!std::isfinite(d))return false;distance+=d*d;}
|
||||
if(distance>.0025f*.0025f)return false;
|
||||
const auto rotation=pose::QuatMultiply(pose::QuatConjugate(p),c);
|
||||
if(std::abs(rotation.w)<.99905f)return false; // Reject large jumps (>5 degrees/frame).
|
||||
float f[4],old[4];
|
||||
for(int i=0;i<4;++i){f[i]=std::tan(current.fov[i]);old[i]=std::tan(previous.fov[i]);if(!std::isfinite(f[i]+old[i]))return false;}
|
||||
if(f[1]<=f[0] || f[2]<=f[3] || old[1]<=old[0] || old[2]<=old[3])return false;
|
||||
const pose::Vec3 basis[3]={{f[1]-f[0],0,0},{0,f[3]-f[2],0},{f[0],f[2],-1}};
|
||||
for(int column=0;column<3;++column){
|
||||
const auto ray=pose::QuatRotate(rotation,basis[column]);
|
||||
rows[0][column]=(ray.x+old[0]*ray.z)/(old[1]-old[0]);
|
||||
rows[1][column]=(-old[2]*ray.z-ray.y)/(old[2]-old[3]);
|
||||
rows[2][column]=-ray.z;
|
||||
}
|
||||
for(int i=0;i<3;++i)rows[i][3]=0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ enum class Stage {
|
||||
kMirrorPublish,
|
||||
kEvidence,
|
||||
kControllerSync,
|
||||
kPresentDependencies,
|
||||
kDesktopMirror,
|
||||
kPresentPrepare,
|
||||
kPresentInterval,
|
||||
|
||||
@@ -29,7 +29,9 @@ public:
|
||||
// Creates the action set/actions and suggests bindings. Instance-level:
|
||||
// valid before any session exists. Failure is non-fatal for XR output; the
|
||||
// backend logs and continues without controller input.
|
||||
Status Create(void *xr_instance);
|
||||
Status Create(void *xr_instance, bool enable_eye_gaze=false);
|
||||
struct EyeGazePose {float orientation[4]={0,0,0,1};bool valid=false;std::uint64_t frame=0;};
|
||||
const EyeGazePose& eye_gaze() const {return eye_gaze_;}
|
||||
|
||||
// Attaches to a session and creates the per-hand pose spaces. Must be
|
||||
// called after xrCreateSession and before the first Sync; recreated on every
|
||||
@@ -90,6 +92,7 @@ private:
|
||||
ControllerStateBuilder builder_;
|
||||
ControllerSnapshot snapshot_;
|
||||
VrHandPose hands_[2];
|
||||
EyeGazePose eye_gaze_;
|
||||
ControllerDeliveryQueue delivery_queue_;
|
||||
std::uint64_t session_generation_ = 0;
|
||||
// Last connection level handed to the consumer, to mark connection changes.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
|
||||
// Runtime-owned eye-tracked density maps. The unused control color images are
|
||||
// never submitted; ordinary output swapchains retain their transfer-copy path.
|
||||
// Caller serializes access and completes GPU use before EndFrame/Destroy.
|
||||
class XrFoveationMaps {
|
||||
public:
|
||||
XrFoveationMaps();
|
||||
~XrFoveationMaps();
|
||||
XrFoveationMaps(const XrFoveationMaps&) = delete;
|
||||
XrFoveationMaps& operator=(const XrFoveationMaps&) = delete;
|
||||
Status Initialize(void* instance, std::uint64_t system, void* session,
|
||||
VkDevice device, PFN_vkGetDeviceProcAddr get_device_proc,
|
||||
std::uint32_t width, std::uint32_t height, VkFormat format);
|
||||
bool BeginFrame();
|
||||
void EndFrame();
|
||||
void Destroy();
|
||||
bool ready() const;
|
||||
bool acquired() const;
|
||||
bool tracking() const;
|
||||
VkImageView view(int eye) const;
|
||||
std::uint32_t width() const;
|
||||
std::uint32_t height() const;
|
||||
bool center(int eye, float* x, float* y) const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
} // namespace mocktail::vr
|
||||
@@ -13,8 +13,8 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Nightcap defaults this on and keeps it on only outside games; a user value
|
||||
// set in fflags.json or the overrides environment disables the switching.
|
||||
// A user value in fflags.json or the overrides environment disables switching.
|
||||
// ARM64 prepares the writable flag before enabling menu code generation.
|
||||
inline constexpr std::string_view kLuauCodegenAllClientSetting =
|
||||
"FFlagDebugLuauCodegenAll";
|
||||
inline constexpr std::string_view kLuauCodegenAllFastFlagName =
|
||||
@@ -38,8 +38,8 @@ bool ClientSettingsSetLuauCodegenAll(std::string_view overrides_json);
|
||||
|
||||
bool LuauCodegenAllForPhase(RobloxExperiencePresencePhase phase);
|
||||
|
||||
// Locates the flag on the first notification; a byte that is not 0 or 1 is
|
||||
// left alone.
|
||||
// Locates the flag once; a missing/ambiguous record or non-boolean byte is left
|
||||
// alone. Automatic ARM64 startup must not force the flag on before preparation.
|
||||
class LuauCodegenMenuSwitch {
|
||||
public:
|
||||
using LocateFlagFn = std::function<std::uint8_t*()>;
|
||||
@@ -47,6 +47,9 @@ class LuauCodegenMenuSwitch {
|
||||
LuauCodegenMenuSwitch(LocateFlagFn locate_flag,
|
||||
RobloxExperiencePresenceObserver next);
|
||||
|
||||
// Call after constructors/settings initialization, before starting LuaApp.
|
||||
// Does not notify the downstream presence observer. False leaves it unchanged.
|
||||
bool PrepareForMenus();
|
||||
RobloxExperiencePresenceObserver observer();
|
||||
void OnPhase(RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
@@ -54,6 +57,7 @@ class LuauCodegenMenuSwitch {
|
||||
private:
|
||||
static void Notify(void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
bool SetEnabledLocked(bool enabled);
|
||||
|
||||
std::mutex mutex_;
|
||||
LocateFlagFn locate_flag_;
|
||||
|
||||
@@ -64,10 +64,12 @@ bool MergeRuntimeClientSettingsOverrides(
|
||||
std::string_view base_json, std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides = nullptr);
|
||||
|
||||
// The resolved VR mode owns this flag in both client-settings channels.
|
||||
// The resolved VR mode owns these flags in both client-settings channels.
|
||||
// Temporal depth preservation also requires non-memoryless native targets.
|
||||
bool MergeVrClientSettingsOverrides(bool enabled, std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
std::string* error,
|
||||
bool preserve_temporal_depth = false);
|
||||
|
||||
// Keeps the legacy test bypass disabled. The host PermissionsProtocol now
|
||||
// answers microphone authorization from capture configuration. No permission
|
||||
|
||||
@@ -36,7 +36,8 @@ DesktopAppPolicyResult ApplyDesktopAppPolicy(
|
||||
bool MergeDesktopAppPolicyClientSettingsOverride(std::string_view policy_json,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
std::string* error,
|
||||
bool default_codegen_at_startup = true);
|
||||
|
||||
// Apply after desktop policy composition. Roblox 2998's Android payload is not
|
||||
// a VRAppBuild, so its LuaApp enables RunService's idle render throttle
|
||||
|
||||
@@ -106,6 +106,7 @@ class RuntimeConfig {
|
||||
|
||||
bool headless() const { return headless_; }
|
||||
bool vr_enabled() const { return vr_enabled_; }
|
||||
bool vr_desktop_mirror() const { return vr_desktop_mirror_; }
|
||||
// MOCKTAIL_VR_ENABLED (--vr, --no-vr) wins over MOCKTAIL_VR_MODE.
|
||||
VrMode vr_mode() const { return vr_mode_; }
|
||||
bool vr_valid() const { return vr_valid_; }
|
||||
@@ -184,6 +185,7 @@ class RuntimeConfig {
|
||||
private:
|
||||
bool headless_ = false;
|
||||
bool vr_enabled_ = kDefaultVrEnabled;
|
||||
bool vr_desktop_mirror_ = true;
|
||||
VrMode vr_mode_ = kDefaultVrEnabled ? VrMode::kOn : VrMode::kOff;
|
||||
bool vr_valid_ = true;
|
||||
std::filesystem::path roblox_library_path_ = "rbx_bin/libroblox.so";
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See the top-level README "About this fork". -->
|
||||
# FreeBSD Linuxulator
|
||||
|
||||
FreeBSD Linuxulator support is experimental. I tested it on FreeBSD
|
||||
15.1-RELEASE-p2 with a Fedora 44 x86_64 userspace.
|
||||
|
||||
FreeBSD returns the wrong filesystem type for `/proc`. Roblox treats this as a
|
||||
broken install and disconnects with Error 304 after about a minute.
|
||||
[`linuxulator.patch`](linuxulator.patch) makes Linuxulator return the same
|
||||
value as Linux.
|
||||
|
||||
## Patch FreeBSD
|
||||
|
||||
Use a clean Git checkout that exactly matches the running FreeBSD kernel. The
|
||||
tested source revision is `aadd58dddcbc78f4d5594827b46b5633552b15ce`. Run
|
||||
the following commands as root.
|
||||
|
||||
```sh
|
||||
fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/freebsd/linuxulator.patch
|
||||
cd /usr/src
|
||||
git apply /root/linuxulator.patch
|
||||
make -j2 kernel-toolchain
|
||||
make -j2 buildkernel KERNCONF=GENERIC MODULES_OVERRIDE=linux64
|
||||
```
|
||||
|
||||
Install only the rebuilt module. Keep a backup and reboot instead of unloading
|
||||
a live Linuxulator module.
|
||||
|
||||
```sh
|
||||
cp -p /boot/kernel/linux64.ko /boot/kernel/linux64.ko.mocktail-backup
|
||||
install -o root -g wheel -m 0444 /usr/obj/usr/src/amd64.amd64/sys/GENERIC/modules/usr/src/sys/modules/linux64/linux64.ko /boot/kernel/linux64.ko.mocktail-new
|
||||
mv /boot/kernel/linux64.ko.mocktail-new /boot/kernel/linux64.ko
|
||||
kldxref /boot/kernel
|
||||
reboot
|
||||
```
|
||||
|
||||
After reboot, verify the result from the Fedora userspace. It must print
|
||||
`9fa0`.
|
||||
|
||||
```sh
|
||||
chroot /compat/linux stat -f -c %t /proc
|
||||
```
|
||||
|
||||
## Run Nightcap
|
||||
|
||||
You can simply run the Nightcap AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest)
|
||||
inside the Fedora userspace.
|
||||
|
||||
## Audio
|
||||
|
||||
Install the ALSA OSS plugin inside the Fedora userspace.
|
||||
|
||||
```sh
|
||||
dnf install -y alsa-lib alsa-plugins-oss
|
||||
```
|
||||
|
||||
On the FreeBSD host, use `cat /dev/sndstat` to find the correct audio device.
|
||||
The number is system-specific: it may be `/dev/dsp0`, `/dev/dsp1`,
|
||||
`/dev/dsp2`, or another device. Replace `/dev/dsp3` below with yours.
|
||||
|
||||
```sh
|
||||
cat << 'EOF' > /etc/asound.conf
|
||||
pcm.!default {
|
||||
type oss
|
||||
device /dev/dsp3
|
||||
}
|
||||
ctl.!default {
|
||||
type oss
|
||||
device /dev/dsp3
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Launch Nightcap with the ALSA audio driver.
|
||||
|
||||
```sh
|
||||
SDL_AUDIO_DRIVER=alsa ./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Nightcap cannot override this check because Roblox reads `/proc` directly. The
|
||||
Linuxulator patch fixes the value before Roblox sees it.
|
||||
@@ -28,12 +28,16 @@
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/CoderDayton/nightcap/main/assets/screenshots/flatpak-home.png</image>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/CoderDayton/nightcap/main/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Murder Mystery 2 on Linux</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Tower of Hell on Linux</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">io.github.CoderDayton.nightcap.desktop</launchable>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM registry.fedoraproject.org/fedora:44
|
||||
|
||||
RUN dnf -y install \
|
||||
gcc-c++ cmake ninja-build git pkgconf-pkg-config lld binutils python3 \
|
||||
glslang spirv-tools \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel json-devel \
|
||||
libyaml-devel libpng-devel elfutils-libelf-devel minizip-ng-compat-devel \
|
||||
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
|
||||
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
|
||||
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel \
|
||||
mesa-dri-drivers mesa-vulkan-drivers mesa-libEGL mesa-libGL mesa-libgbm \
|
||||
pipewire-libs pulseaudio-libs alsa-lib dbus-libs libnotify \
|
||||
&& dnf clean all
|
||||
|
||||
WORKDIR /work
|
||||
COPY . /work
|
||||
RUN bash /work/build-frame-nightcap.sh
|
||||
|
||||
ENV PATH="/opt/nightcap-vr-arm64/bin:${PATH}"
|
||||
ENTRYPOINT ["/opt/nightcap-vr-arm64/lib64/mocktail/mocktail"]
|
||||
CMD ["--vr"]
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run without host mounts, a GPU, SteamVR, account data or network access.
|
||||
image=${1:-localhost/nightcap-vr-arm64:frame-foveated}
|
||||
platform=$(podman image inspect --format '{{.Os}}/{{.Architecture}}' "$image")
|
||||
if [[ "$platform" != linux/arm64 ]]; then
|
||||
printf 'Expected linux/arm64 image, got %s\n' "$platform" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
podman run --rm -i --network none --read-only --cap-drop all \
|
||||
--security-opt no-new-privileges --tmpfs /tmp:rw,nosuid,nodev,size=16m \
|
||||
--entrypoint python3 "$image" - <<'PY'
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
prefix = Path('/opt/nightcap-vr-arm64')
|
||||
runtime = prefix / 'lib64/mocktail'
|
||||
programs = [
|
||||
prefix / 'bin/mocktail',
|
||||
prefix / 'bin/mocktail-vr-probe',
|
||||
prefix / 'bin/nightcap-vr-settings',
|
||||
prefix / 'bin/nightcap-vr-performance',
|
||||
runtime / 'mocktail',
|
||||
runtime / 'mocktail_updater',
|
||||
runtime / 'mocktail_failure_dialog',
|
||||
runtime / 'mocktail_webview_helper',
|
||||
]
|
||||
libraries = list(runtime.glob('*.so'))
|
||||
if len(libraries) < 12:
|
||||
raise SystemExit('Installed Android adapter libraries are missing')
|
||||
for path in programs + libraries:
|
||||
if not path.is_file() or not os.access(path, os.X_OK):
|
||||
raise SystemExit(f'Missing executable: {path}')
|
||||
with path.open('rb') as stream:
|
||||
header = stream.read(20)
|
||||
if (header[:6] != b'\x7fELF\x02\x01' or len(header) != 20
|
||||
or struct.unpack_from('<H', header, 18)[0] != 183):
|
||||
raise SystemExit(f'Expected a little-endian AArch64 ELF: {path}')
|
||||
result = subprocess.run(['ldd', str(path)], capture_output=True, text=True,
|
||||
timeout=20, check=False)
|
||||
if result.returncode or 'not found' in result.stdout + result.stderr:
|
||||
raise SystemExit(f'Unresolved runtime dependencies: {path}\n'
|
||||
+ result.stdout + result.stderr)
|
||||
|
||||
metadata = prefix / 'share/mocktail/metadata'
|
||||
for name in ('roblox_compatibility.json', 'roblox_bootstrap_sources.json',
|
||||
'roblox_host_abi_reference.json', 'roblox_signing_certificates.json'):
|
||||
value = json.loads((metadata / name).read_text())
|
||||
if not isinstance(value, (dict, list)) or not value:
|
||||
raise SystemExit(f'Invalid installed metadata: {name}')
|
||||
license_files = [Path('/work/LICENSE'), Path('/work/THIRD_PARTY.md')]
|
||||
for path in license_files:
|
||||
if not path.is_file() or not path.stat().st_size:
|
||||
raise SystemExit(f'Missing source license notices: {path}')
|
||||
|
||||
def verify_native_cmake_settings(cache_text):
|
||||
settings = {}
|
||||
for line in cache_text.splitlines():
|
||||
if not line or line.startswith(('#', '//')):
|
||||
continue
|
||||
declaration, separator, value = line.partition('=')
|
||||
name, type_separator, cache_type = declaration.partition(':')
|
||||
if separator and type_separator and cache_type:
|
||||
settings[name] = value
|
||||
# VR is a STRING option (AUTO/ON/OFF), while the other flags are BOOL.
|
||||
# The configured value determines the build policy, not the cache type.
|
||||
required = {'MOCKTAIL_ENABLE_VR': 'ON',
|
||||
'MOCKTAIL_ENABLE_FSR3': 'OFF',
|
||||
'MOCKTAIL_VR_DIAGNOSTICS': 'OFF'}
|
||||
for name, expected in required.items():
|
||||
if settings.get(name) != expected:
|
||||
raise SystemExit(f'Unexpected build setting: {name}={settings.get(name)!r}; expected {expected}')
|
||||
|
||||
|
||||
cache = Path('/work/build-frame-arm64/CMakeCache.txt')
|
||||
build_policy = 'source-build-cache'
|
||||
if cache.exists():
|
||||
verify_native_cmake_settings(cache.read_text())
|
||||
else:
|
||||
# Source-audited release images may omit their disposable compiler cache.
|
||||
provenance = json.loads((metadata / 'steam-frame-source.json').read_text())
|
||||
if provenance.get('fsr3_compiled') is not False or provenance.get('upscaling') != 'Native':
|
||||
raise SystemExit('Missing Native-only build provenance')
|
||||
build_policy = 'installed-source-provenance'
|
||||
|
||||
for path in ('/opt/steamvr', '/opt/host-lib', '/work/data', '/work/runtime',
|
||||
'/work/rbx_bin', '/root/.config/mocktail',
|
||||
'/root/.local/share/mocktail'):
|
||||
if Path(path).exists():
|
||||
raise SystemExit(f'Host runtime or user data must not be packaged: {path}')
|
||||
patterns = ('libroblox.so', '*.apk', '*.xapk', '*.aab', '*.obb',
|
||||
'cookies*.txt', 'steamclient.so', 'vrclient.so')
|
||||
# This upstream archive-parser regression fixture is not a Roblox client.
|
||||
archive_fixture = Path('/work/third_party/mcpelauncher-linker/core/libziparchive/testdata/crash.apk')
|
||||
for root in ('/work', '/opt', '/root', '/home'):
|
||||
for directory, subdirectories, filenames in os.walk(root, followlinks=False):
|
||||
if '.git' in subdirectories or '.git' in filenames:
|
||||
raise SystemExit('Git checkout credentials/history must not enter the image')
|
||||
for name in filenames:
|
||||
path = Path(directory) / name
|
||||
if path == archive_fixture:
|
||||
continue
|
||||
if any(fnmatch.fnmatch(name.lower(), pattern) for pattern in patterns):
|
||||
raise SystemExit(f'Proprietary payload or account data in image: {path}')
|
||||
|
||||
print(json.dumps({
|
||||
'platform': 'linux/arm64',
|
||||
'native_only_build_policy': build_policy,
|
||||
'aarch64_programs_checked': len(programs),
|
||||
'aarch64_adapter_libraries_checked': len(libraries),
|
||||
'dynamic_dependencies': 'resolved',
|
||||
'client_payload': 'absent',
|
||||
'host_steamvr_and_driver_mounts': 'absent',
|
||||
'license_sha256': {str(path): hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for path in license_files},
|
||||
'scope': 'offline packaging checks; no headset or Roblox session started',
|
||||
}, indent=2))
|
||||
PY
|
||||
@@ -62,7 +62,7 @@ Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/CoderDayton/nightcap
|
||||
Comment=Roblox on Linux, tuned for desktop play
|
||||
Comment=Roblox on Linux, tuned for a real PC
|
||||
Icon=${BASE_URL}/nightcap.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
@@ -72,20 +72,3 @@ install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/terms.html" "${OUTPUT}/terms.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/privacy.html" "${OUTPUT}/privacy.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/preview.png" "${OUTPUT}/preview.png"
|
||||
|
||||
cat >"${OUTPUT}/robots.txt" <<EOF
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${BASE_URL}/sitemap.xml
|
||||
EOF
|
||||
|
||||
cat >"${OUTPUT}/sitemap.xml" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>${BASE_URL}/</loc></url>
|
||||
<url><loc>${BASE_URL}/terms.html</loc></url>
|
||||
<url><loc>${BASE_URL}/privacy.html</loc></url>
|
||||
</urlset>
|
||||
EOF
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate src/vr/vr_shaders_spirv.h from shaders/vr with glslc.
|
||||
|
||||
The header is checked in so a build never needs a shader compiler. The SMAA
|
||||
passes include SMAA.hlsl from the third_party/smaa submodule. Run this
|
||||
The header is checked in so a build never needs a shader compiler. Run this
|
||||
after editing a shader; VrShaders.SpirvMatchesSources checks it when glslc is
|
||||
installed.
|
||||
"""
|
||||
@@ -18,15 +17,7 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
SHADERS = [
|
||||
("kFullscreenVertexSpirv", "shaders/vr/fullscreen.vert"),
|
||||
("kFxaaFragmentSpirv", "shaders/vr/fxaa.frag"),
|
||||
("kSmaaEdgesVertexSpirv", "shaders/vr/smaa_edges.vert"),
|
||||
("kSmaaEdgesFragmentSpirv", "shaders/vr/smaa_edges.frag"),
|
||||
("kSmaaWeightsVertexSpirv", "shaders/vr/smaa_weights.vert"),
|
||||
("kSmaaWeightsFragmentSpirv", "shaders/vr/smaa_weights.frag"),
|
||||
("kSmaaBlendVertexSpirv", "shaders/vr/smaa_blend.vert"),
|
||||
("kSmaaBlendFragmentSpirv", "shaders/vr/smaa_blend.frag"),
|
||||
]
|
||||
# SMAA.hlsl comes from the third_party/smaa submodule.
|
||||
INCLUDES = [ROOT / "third_party/smaa"]
|
||||
OUTPUT = ROOT / "src/vr/vr_shaders_spirv.h"
|
||||
|
||||
|
||||
@@ -34,9 +25,8 @@ def compile_words(glslc, source):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = pathlib.Path(directory) / "shader.spv"
|
||||
subprocess.run(
|
||||
[glslc, "-O", "--target-env=vulkan1.0"] +
|
||||
["-I" + str(path) for path in INCLUDES] +
|
||||
[str(ROOT / source), "-o", str(output)],
|
||||
[glslc, "-O", "--target-env=vulkan1.0", str(ROOT / source), "-o",
|
||||
str(output)],
|
||||
check=True)
|
||||
data = output.read_bytes()
|
||||
return struct.unpack("<%dI" % (len(data) // 4), data)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_VS 0
|
||||
#include "SMAA.hlsl"
|
||||
layout(set = 0, binding = 0) uniform sampler2D colorTex;
|
||||
layout(set = 0, binding = 1) uniform sampler2D blendTex;
|
||||
layout(location = 0) in vec2 texcoord;
|
||||
layout(location = 1) in vec4 offset;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
// The target encodes linear values back to sRGB, which restores these bytes.
|
||||
vec3 Linearize(vec3 c) {
|
||||
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)),
|
||||
step(vec3(0.04045), c));
|
||||
}
|
||||
|
||||
void main() {
|
||||
color = SMAANeighborhoodBlendingPS(texcoord, offset, colorTex, blendTex);
|
||||
if (push.linearize != 0u) color.rgb = Linearize(color.rgb);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_PS 0
|
||||
#include "SMAA.hlsl"
|
||||
layout(location = 0) out vec2 texcoord;
|
||||
layout(location = 1) out vec4 offset;
|
||||
|
||||
void main() {
|
||||
texcoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
gl_Position = vec4(texcoord * 2.0 - 1.0, 0.0, 1.0);
|
||||
SMAANeighborhoodBlendingVS(texcoord, offset);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Shared setup for the SMAA passes: the upstream SMAA.hlsl (third_party/smaa)
|
||||
// in its GLSL 4 mode at the High preset. Vulkan, like Direct3D, puts the
|
||||
// texture origin at the top left, which is what SMAA and its lookup textures
|
||||
// expect, so nothing is flipped.
|
||||
layout(push_constant) uniform Push {
|
||||
vec4 metrics; // 1/width, 1/height, width, height
|
||||
uint linearize; // 1 when the target view re-encodes to sRGB
|
||||
} push;
|
||||
#define SMAA_RT_METRICS push.metrics
|
||||
#define SMAA_GLSL_4 1
|
||||
#define SMAA_PRESET_HIGH 1
|
||||
@@ -1,12 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_VS 0
|
||||
#include "SMAA.hlsl"
|
||||
// Luma edges on the eye's sRGB-encoded bytes, as SMAA recommends.
|
||||
layout(set = 0, binding = 0) uniform sampler2D colorTex;
|
||||
layout(location = 0) in vec2 texcoord;
|
||||
layout(location = 1) in vec4 offset[3];
|
||||
layout(location = 0) out vec2 edges;
|
||||
|
||||
void main() { edges = SMAALumaEdgeDetectionPS(texcoord, offset, colorTex); }
|
||||
@@ -1,13 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_PS 0
|
||||
#include "SMAA.hlsl"
|
||||
layout(location = 0) out vec2 texcoord;
|
||||
layout(location = 1) out vec4 offset[3];
|
||||
|
||||
void main() {
|
||||
texcoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
gl_Position = vec4(texcoord * 2.0 - 1.0, 0.0, 1.0);
|
||||
SMAAEdgeDetectionVS(texcoord, offset);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_VS 0
|
||||
#include "SMAA.hlsl"
|
||||
layout(set = 0, binding = 0) uniform sampler2D edgesTex;
|
||||
layout(set = 0, binding = 1) uniform sampler2D areaTex;
|
||||
layout(set = 0, binding = 2) uniform sampler2D searchTex;
|
||||
layout(location = 0) in vec2 texcoord;
|
||||
layout(location = 1) in vec2 pixcoord;
|
||||
layout(location = 2) in vec4 offset[3];
|
||||
layout(location = 0) out vec4 weights;
|
||||
|
||||
void main() {
|
||||
weights = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, edgesTex,
|
||||
areaTex, searchTex, vec4(0.0));
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : require
|
||||
#include "smaa_common.glsl"
|
||||
#define SMAA_INCLUDE_PS 0
|
||||
#include "SMAA.hlsl"
|
||||
layout(location = 0) out vec2 texcoord;
|
||||
layout(location = 1) out vec2 pixcoord;
|
||||
layout(location = 2) out vec4 offset[3];
|
||||
|
||||
void main() {
|
||||
texcoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
gl_Position = vec4(texcoord * 2.0 - 1.0, 0.0, 1.0);
|
||||
SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset);
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
<!doctype html>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Nightcap: Roblox on Linux</title>
|
||||
<meta name="description" content="Roblox on Linux, tuned for desktop play. A Mocktail fork that keeps up with Roblox updates, with extra bug fixes, custom assets and more.">
|
||||
<meta name="google-site-verification" content="vznL8W2txRlmfwhGg7ZBPSxQaPV9OtaIUoYPvmd9aUA">
|
||||
<link rel="canonical" href="https://coderdayton.github.io/nightcap/">
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Nightcap">
|
||||
<meta property="og:title" content="Nightcap: Roblox on Linux">
|
||||
<meta property="og:description" content="Roblox on Linux, tuned for desktop play. A Mocktail fork that keeps up with Roblox updates, with extra bug fixes, custom assets and more.">
|
||||
<meta property="og:url" content="https://coderdayton.github.io/nightcap/">
|
||||
<meta property="og:image" content="https://coderdayton.github.io/nightcap/preview.png">
|
||||
<meta property="og:image:alt" content="Roblox running on Linux with Nightcap">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Nightcap",
|
||||
"description": "Roblox on Linux, tuned for desktop play. A Mocktail fork that keeps up with Roblox updates, with extra bug fixes, custom assets and more.",
|
||||
"applicationCategory": "GameApplication",
|
||||
"operatingSystem": "Linux",
|
||||
"url": "https://coderdayton.github.io/nightcap/",
|
||||
"downloadUrl": "https://github.com/CoderDayton/nightcap/releases/latest",
|
||||
"license": "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero">
|
||||
<div class="wrap">
|
||||
<p class="top">
|
||||
<a href="https://github.com/CoderDayton/nightcap" aria-label="Nightcap on GitHub" title="GitHub"><svg viewBox="0 0 16 16" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a>
|
||||
<a href="https://discord.gg/DbAUG9hBFU" aria-label="Nightcap on Discord" title="Discord"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z"/></svg></a>
|
||||
</p>
|
||||
<p class="eyebrow">Roblox on Linux, tuned for desktop play</p>
|
||||
<h1><img src="nightcap.svg" alt=""><span>Nightcap</span></h1>
|
||||
<p class="lede">A Mocktail fork that keeps up with Roblox updates, with extra bug fixes, custom assets and more.</p>
|
||||
<p class="actions">
|
||||
<a class="button" href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage">Download AppImage</a>
|
||||
<a class="button secondary" href="nightcap.flatpakref">Install with Flatpak</a>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="row stacked">
|
||||
<h2>Install</h2>
|
||||
<dl class="body install">
|
||||
<dt>AppImage</dt>
|
||||
<dd><pre><code>chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage</code></pre></dd>
|
||||
<dt>Flatpak</dt>
|
||||
<dd><pre><code>flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap</code></pre></dd>
|
||||
<dt>DEB, RPM, pacman</dt>
|
||||
<dd><pre><code>sudo apt install ./nightcap_<version>-1_amd64.deb
|
||||
sudo dnf install ./nightcap-<version>-1.x86_64.rpm
|
||||
sudo pacman -U nightcap-<version>-1-x86_64.pkg.tar.zst</code></pre></dd>
|
||||
<dt>Source</dt>
|
||||
<dd><pre><code>git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap && make build
|
||||
./build/mocktail</code></pre></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<h2>What's different</h2>
|
||||
<dl class="body facts">
|
||||
<dt>Roblox updates</dt>
|
||||
<dd>New Roblox versions work soon after they come out.</dd>
|
||||
<dt>More games</dt>
|
||||
<dd>Answers a server check some experiences use, so you can play those too.</dd>
|
||||
<dt>Voice chat</dt>
|
||||
<dd>Your microphone still works after a Roblox update.</dd>
|
||||
<dt>Login</dt>
|
||||
<dd>Verification checks and captchas finish instead of hanging.</dd>
|
||||
<dt>Look</dt>
|
||||
<dd>The new home screen and side menu, like on Windows.</dd>
|
||||
<dt>Your own assets</dt>
|
||||
<dd>Drop in your own cursors, fonts and skies. The folder layout matches Sober's.</dd>
|
||||
<dt>Mouse and CPU</dt>
|
||||
<dd>Optional raw mouse input, and less CPU use while idle.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<h2>Direct downloads</h2>
|
||||
<div class="body">
|
||||
<p>Prebuilt builds for Linux x86_64.</p>
|
||||
<ul class="links">
|
||||
<li><a href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage"><span class="name">AppImage</span><span class="file">Nightcap-x86_64.AppImage</span><span class="go" aria-hidden="true">↓</span></a></li>
|
||||
<li><a href="Nightcap-x86_64.flatpak"><span class="name">Flatpak bundle</span><span class="file">Nightcap-x86_64.flatpak</span><span class="go" aria-hidden="true">↓</span></a></li>
|
||||
<li><a href="https://github.com/CoderDayton/nightcap/releases/latest"><span class="name">DEB, RPM, pacman</span><span class="file">GitHub releases page</span><span class="go" aria-hidden="true">↗</span></a></li>
|
||||
<li><a href="nightcap-flatpak.gpg"><span class="name">Signing key</span><span class="file">nightcap-flatpak.gpg</span><span class="go" aria-hidden="true">↓</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="wrap foot">
|
||||
<p>
|
||||
<a href="https://github.com/CoderDayton/nightcap">Source code</a> ·
|
||||
<a href="https://github.com/komaruworld/mocktail">Upstream Mocktail</a> ·
|
||||
<a href="terms.html">Terms</a> ·
|
||||
<a href="privacy.html">Privacy</a>
|
||||
</p>
|
||||
<p>Nightcap is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Adds a copy button to each command block. The page works without it.
|
||||
document.querySelectorAll('.install dd').forEach(function (block) {
|
||||
var button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = 'copy';
|
||||
button.addEventListener('click', function () {
|
||||
if (!navigator.clipboard) return;
|
||||
navigator.clipboard.writeText(block.querySelector('code').textContent).then(function () {
|
||||
button.textContent = 'copied';
|
||||
setTimeout(function () { button.textContent = 'copy'; }, 1600);
|
||||
});
|
||||
});
|
||||
block.appendChild(button);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 419 KiB |
@@ -1,53 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Nightcap Privacy Policy</title>
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="wrap bar">
|
||||
<a class="brand" href="index.html"><img src="nightcap.svg" alt="" width="30" height="30"><span>Nightcap</span></a>
|
||||
<p class="top">
|
||||
<a href="https://github.com/CoderDayton/nightcap" aria-label="Nightcap on GitHub" title="GitHub"><svg viewBox="0 0 16 16" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a>
|
||||
<a href="https://discord.gg/DbAUG9hBFU" aria-label="Nightcap on Discord" title="Discord"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z"/></svg></a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main class="wrap prose">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Nightcap is a Linux runtime for the Roblox Android client. This policy covers Nightcap itself and its Discord application. Roblox and Discord have their own policies for the data they handle.</p>
|
||||
|
||||
<h2>What Nightcap collects</h2>
|
||||
<p>Nothing. Nightcap has no server. It does not collect, store, or send telemetry, analytics, crash reports, or account data to the Nightcap project.</p>
|
||||
|
||||
<h2>What stays on your machine</h2>
|
||||
<ul>
|
||||
<li>Configuration in <code>~/.config/mocktail/</code></li>
|
||||
<li>Session logs and support bundles in <code>~/.local/state/mocktail/</code></li>
|
||||
<li>Roblox login session under <code>~/.local/share/mocktail/</code></li>
|
||||
<li>Roblox cache, downloaded payloads, and shader cache under <code>~/.cache/mocktail/</code></li>
|
||||
</ul>
|
||||
<p>Flatpak installs keep the same layout under <code>~/.var/app/io.github.CoderDayton.nightcap/</code>.</p>
|
||||
|
||||
<h2>Discord Rich Presence</h2>
|
||||
<p>When Rich Presence is on, Nightcap sends the current experience name, its public icon URL, elapsed time, and an optional public server join code to the Discord desktop client on the same machine over a local socket. Discord displays this on your profile under its own privacy settings. Nightcap never signs in to Discord, never reads a Discord token, and never contacts Discord's servers.</p>
|
||||
<p>Set <code>integrations.discord_rpc.enabled: false</code> in <code>config.yaml</code> to turn it off.</p>
|
||||
|
||||
<h2>Roblox</h2>
|
||||
<p>Nightcap talks to Roblox's services exactly as the Roblox client would. That traffic is covered by the Roblox Privacy Policy.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Open an issue at <a href="https://github.com/CoderDayton/nightcap/issues">github.com/CoderDayton/nightcap/issues</a>.</p>
|
||||
|
||||
<p class="legal-nav">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="terms.html">Terms of Service</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
-305
@@ -1,305 +0,0 @@
|
||||
/* Modified by vii from komaruworld/mocktail. See README "About this fork". */
|
||||
:root {
|
||||
--bg: #14122b;
|
||||
--ink: #ece8ff;
|
||||
--muted: #b3aad6;
|
||||
--accent: #6c4fd1;
|
||||
--link: #a78bfa;
|
||||
--line: #3a3470;
|
||||
--code-bg: #1d1a3d;
|
||||
font: 17px/1.6 system-ui, sans-serif;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
a { color: var(--link); }
|
||||
a:focus-visible,
|
||||
button:focus-visible { outline: 2px solid var(--link); outline-offset: 3px; }
|
||||
|
||||
.wrap {
|
||||
position: relative;
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero { padding-bottom: clamp(48px, 10vh, 110px); }
|
||||
|
||||
.top {
|
||||
margin: 0;
|
||||
padding-top: 22px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.top a {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.top a:hover { color: var(--ink); }
|
||||
|
||||
.eyebrow {
|
||||
display: inline-block;
|
||||
margin: clamp(48px, 12vh, 130px) 0 0;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--code-bg);
|
||||
color: var(--link);
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 18px 0 0;
|
||||
font-size: clamp(3.6rem, 15vw, 9.5rem);
|
||||
font-weight: 800;
|
||||
line-height: .95;
|
||||
letter-spacing: -.045em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .16em;
|
||||
}
|
||||
|
||||
.hero h1 img {
|
||||
height: .78em;
|
||||
width: auto;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 34ch;
|
||||
margin: 22px 0 0;
|
||||
color: var(--ink);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.actions { margin: 28px 0 0; }
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
margin: 0 10px 10px 0;
|
||||
padding: 12px 20px;
|
||||
border: 2px solid var(--accent);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.button:hover { background: #7d62dd; border-color: #7d62dd; }
|
||||
|
||||
a.button.secondary {
|
||||
background: var(--bg);
|
||||
border-color: var(--link);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
a.button.secondary:hover { background: var(--code-bg); }
|
||||
|
||||
/* Every section is a label on the left and its content on the right. */
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 8px 32px;
|
||||
padding: 36px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row.stacked { grid-template-columns: 1fr; gap: 20px; }
|
||||
|
||||
.body { margin: 0; min-width: 0; }
|
||||
.body p { margin: 0 0 12px; color: var(--muted); }
|
||||
|
||||
dl.body {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 0 28px;
|
||||
}
|
||||
|
||||
dt {
|
||||
padding: 9px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
dd {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 9px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.facts dt,
|
||||
.facts dd { border-bottom: 1px solid var(--line); }
|
||||
.facts dt:last-of-type,
|
||||
.facts dd:last-of-type { border-bottom: 0; }
|
||||
.facts dt { color: var(--ink); font-weight: 700; }
|
||||
|
||||
dl.body.install { display: block; }
|
||||
|
||||
.install dt {
|
||||
padding: 18px 0 6px;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.install dt:first-child { padding-top: 0; }
|
||||
.install dd { padding: 0; }
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
overflow-x: auto;
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
code {
|
||||
font: .9rem/1.7 ui-monospace, "DejaVu Sans Mono", "JetBrains Mono", monospace;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
dd button {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
right: 0;
|
||||
padding: 2px 0 2px 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: .8rem ui-monospace, monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
dd button:hover { color: var(--ink); text-decoration: underline; }
|
||||
|
||||
.links {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.links li { border-bottom: 1px solid var(--line); }
|
||||
|
||||
.links a {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 24px;
|
||||
align-items: baseline;
|
||||
gap: 4px 16px;
|
||||
margin: 0 -8px;
|
||||
padding: 12px 24px 12px 8px;
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links a:hover { background: var(--code-bg); }
|
||||
.links a:hover .name { text-decoration: underline; text-decoration-color: var(--link); text-underline-offset: 4px; }
|
||||
|
||||
.links .file {
|
||||
color: var(--muted);
|
||||
font: 400 .88rem ui-monospace, "DejaVu Sans Mono", monospace;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.links .go { color: var(--link); text-align: right; }
|
||||
|
||||
.foot {
|
||||
padding-bottom: 48px;
|
||||
color: var(--muted);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.foot::before {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.foot p { margin: 0 0 8px; }
|
||||
|
||||
/* Terms and privacy pages: a header bar and one readable column. */
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
.bar .top { padding-top: 0; }
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--ink);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -.02em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand img { width: 30px; height: 30px; }
|
||||
|
||||
.prose {
|
||||
padding-top: clamp(32px, 8vh, 80px);
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.prose > * { max-width: 700px; }
|
||||
|
||||
.prose h1 {
|
||||
margin: 0 0 16px;
|
||||
font-size: clamp(2rem, 6vw, 3rem);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -.03em;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
margin: 36px 0 6px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.prose p,
|
||||
.prose li { margin: 0 0 12px; }
|
||||
.prose ul { margin: 0 0 12px; padding-left: 1.2rem; }
|
||||
|
||||
.prose code {
|
||||
padding: 1px 6px;
|
||||
background: var(--code-bg);
|
||||
font-size: .85rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.legal-nav { display: flex; flex-wrap: wrap; gap: 4px 20px; margin-top: 28px; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.row { grid-template-columns: 1fr; padding: 28px 0; }
|
||||
dl.body:not(.install) { grid-template-columns: 1fr; }
|
||||
.facts dt { padding-bottom: 0; border-bottom: 0; }
|
||||
.links a { grid-template-columns: 1fr 24px; }
|
||||
.links .file { grid-column: 1; grid-row: 2; }
|
||||
.links .go { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Nightcap Terms of Service</title>
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="wrap bar">
|
||||
<a class="brand" href="index.html"><img src="nightcap.svg" alt="" width="30" height="30"><span>Nightcap</span></a>
|
||||
<p class="top">
|
||||
<a href="https://github.com/CoderDayton/nightcap" aria-label="Nightcap on GitHub" title="GitHub"><svg viewBox="0 0 16 16" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a>
|
||||
<a href="https://discord.gg/DbAUG9hBFU" aria-label="Nightcap on Discord" title="Discord"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z"/></svg></a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main class="wrap prose">
|
||||
<h1>Terms of Service</h1>
|
||||
<p>Nightcap is free and open-source software licensed under the Apache License, Version 2.0. The license text shipped with the source governs your use of the software.</p>
|
||||
|
||||
<h2>No warranty</h2>
|
||||
<p>Nightcap is provided "as is", without warranty of any kind. The authors are not liable for any loss, damage, or account action that results from using it. See sections 7 and 8 of the Apache License.</p>
|
||||
|
||||
<h2>Not affiliated</h2>
|
||||
<p>Nightcap is an independent community project. It is not affiliated with, endorsed by, or supported by Roblox Corporation or Discord Inc. Roblox and Discord are trademarks of their respective owners.</p>
|
||||
|
||||
<h2>Your responsibility</h2>
|
||||
<p>You are responsible for following the Roblox Terms of Use and the Discord Terms of Service while using Nightcap with those services. Nightcap does not modify Roblox game logic, does not bypass Roblox security, and is not a cheat.</p>
|
||||
|
||||
<h2>Discord application</h2>
|
||||
<p>The Nightcap Discord application only provides Rich Presence for the Nightcap desktop runtime. It requests no permissions, has no bot, and does not access your Discord account.</p>
|
||||
|
||||
<h2>Changes</h2>
|
||||
<p>These terms may change with new releases. The current version always lives at this address.</p>
|
||||
|
||||
<p class="legal-nav">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -33,7 +33,11 @@ constexpr std::size_t kOutputCountVtableIndex = 5;
|
||||
constexpr std::size_t kOutputInfoVtableIndex = 6;
|
||||
constexpr std::size_t kGuestStringSize = 24;
|
||||
constexpr std::size_t kGuestDeviceInfoSize = kGuestStringSize * 2 + 1;
|
||||
#if defined(__aarch64__)
|
||||
constexpr std::size_t kStringConstructorContractSize = 32;
|
||||
#else
|
||||
constexpr std::size_t kStringConstructorContractSize = 24;
|
||||
#endif
|
||||
constexpr std::size_t kMaximumMenuDevices = 128;
|
||||
constexpr std::size_t kMaximumDeviceNameBytes = 512;
|
||||
|
||||
@@ -138,9 +142,16 @@ bool HasExpectedFmodStringConstructorContract(const std::uint8_t* code,
|
||||
std::size_t size) {
|
||||
constexpr std::array<std::uint8_t, kStringConstructorContractSize>
|
||||
kExpectedPrefix = {
|
||||
#if defined(__aarch64__)
|
||||
0xfd,0x7b,0xbd,0xa9,0xf6,0x57,0x01,0xa9,
|
||||
0xf4,0x4f,0x02,0xa9,0xfd,0x03,0x00,0x91,
|
||||
0xf4,0x03,0x02,0xaa,0xf3,0x03,0x01,0xaa,
|
||||
0x5f,0x58,0x00,0xf1,0xf5,0x03,0x00,0xaa,
|
||||
#else
|
||||
0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56,
|
||||
0x41, 0x54, 0x53, 0x48, 0x89, 0xd3, 0x49, 0x89,
|
||||
0xf6, 0x49, 0x89, 0xff, 0x48, 0x83, 0xfa, 0x16,
|
||||
#endif
|
||||
};
|
||||
return code != nullptr && size >= kExpectedPrefix.size() &&
|
||||
std::memcmp(code, kExpectedPrefix.data(), kExpectedPrefix.size()) == 0;
|
||||
@@ -454,6 +465,22 @@ Status RobloxOutputDeviceBridge::PatchVtableLocked() {
|
||||
}
|
||||
|
||||
if (profile_.has_input_devices()) {
|
||||
#if defined(__aarch64__)
|
||||
// The capture ABI was checked against this exact ARM64 payload. Do not
|
||||
// infer capture signatures from neighboring virtual slots on later builds.
|
||||
constexpr std::array<std::uintptr_t,6> capture_2998={
|
||||
0x2f964f4,0x2f96068,0x2f96c74,0x2f97464,0x2f95c64,0x2f97a64};
|
||||
constexpr std::array<std::uintptr_t,6> capture_3222={
|
||||
0x2c356c8,0x2c34c60,0x2c36538,0x2c36b1c,0x2c349ec,0x2c36db8};
|
||||
const bool build2998 = profile_.vtable_rva==0x632aeb0 && profile_.vtable_layout_version==1;
|
||||
const bool build3222 = profile_.vtable_rva==0x6569770 && profile_.vtable_layout_version==2;
|
||||
if(!build2998 && !build3222)
|
||||
return FailedPrecondition("unsupported ARM64 native capture profile");
|
||||
const auto& capture_rvas = build3222 ? capture_3222 : capture_2998;
|
||||
for(std::size_t i=0;i<capture_indexes.size();++i)
|
||||
if(vtable_[capture_indexes[i]]!=library_base_+capture_rvas[i])
|
||||
return FailedPrecondition("ARM64 native capture vtable contract mismatch");
|
||||
#endif
|
||||
for (std::size_t i = 0; i < capture_indexes.size(); ++i) {
|
||||
const auto method = vtable_[capture_indexes[i]];
|
||||
if (method < library_base_ ||
|
||||
@@ -591,18 +618,17 @@ int RobloxOutputDeviceBridge::GetOutputDeviceCount(void* self) {
|
||||
return bridge != nullptr ? bridge->DeviceCount() : 0;
|
||||
}
|
||||
|
||||
void* RobloxOutputDeviceBridge::GetOutputDeviceInfo(void* result, void* self,
|
||||
int index) {
|
||||
RobloxOutputDeviceBridge::DeviceInfoResult
|
||||
RobloxOutputDeviceBridge::GetOutputDeviceInfo(void* self, int index) {
|
||||
DeviceInfoResult result{};
|
||||
static_cast<void>(self);
|
||||
RobloxOutputDeviceBridge* bridge =
|
||||
g_active_bridge.load(std::memory_order_acquire);
|
||||
if (bridge == nullptr) {
|
||||
if (result != nullptr) {
|
||||
std::memset(result, 0, kGuestDeviceInfoSize);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return bridge->DeviceInfo(result, index);
|
||||
bridge->DeviceInfo(result.bytes, index);
|
||||
return result;
|
||||
}
|
||||
|
||||
int RobloxOutputDeviceBridge::GetCurrentOutputDevice(void* self) {
|
||||
@@ -626,13 +652,12 @@ int RobloxOutputDeviceBridge::GetInputDeviceCount(void *) {
|
||||
return bridge ? bridge->DeviceCount(true) : 0;
|
||||
}
|
||||
|
||||
void *RobloxOutputDeviceBridge::GetInputDeviceInfo(void *result, void *,
|
||||
int index) {
|
||||
RobloxOutputDeviceBridge::DeviceInfoResult
|
||||
RobloxOutputDeviceBridge::GetInputDeviceInfo(void*, int index) {
|
||||
DeviceInfoResult result{};
|
||||
auto *bridge = g_active_bridge.load(std::memory_order_acquire);
|
||||
if (bridge)
|
||||
return bridge->DeviceInfo(result, index, true);
|
||||
if (result)
|
||||
std::memset(result, 0, kGuestDeviceInfoSize);
|
||||
bridge->DeviceInfo(result.bytes, index, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "compat/bionic_host_libc_runtime.h"
|
||||
#include "compat/allocator_snapshot_cache.h"
|
||||
#include <chrono>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
@@ -245,10 +247,14 @@ void BionicArc4RandomBuffer(void* buffer, size_t size) noexcept {
|
||||
BionicMallinfoSnapshot BionicMallinfo() noexcept {
|
||||
#if defined(__GLIBC__) && defined(__GLIBC_PREREQ)
|
||||
#if __GLIBC_PREREQ(2, 33)
|
||||
const struct mallinfo2 host = ::mallinfo2();
|
||||
return {host.arena, host.ordblks, host.smblks, host.hblks,
|
||||
host.hblkhd, host.usmblks, host.fsmblks, host.uordblks,
|
||||
host.fordblks, host.keepcost};
|
||||
static AllocatorSnapshotCache cache;
|
||||
const auto now=std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
return cache.Read(static_cast<std::uint64_t>(now),[]() noexcept {
|
||||
const struct mallinfo2 host=::mallinfo2();
|
||||
return BionicMallinfoSnapshot{host.arena,host.ordblks,host.smblks,host.hblks,
|
||||
host.hblkhd,host.usmblks,host.fsmblks,host.uordblks,host.fordblks,host.keepcost};
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
return {};
|
||||
|
||||
@@ -6,6 +6,25 @@ namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
constexpr HostAbiProfile kProfiles[] = {
|
||||
// Roblox 2.742.1059 ARM64. Constructor roles were re-derived: allocator
|
||||
// wiring 4, JNI singleton 5, lazy clock 6, native process load 7.
|
||||
// Preserve the payload's new native replay order and initialize TLS after 7.
|
||||
{"25aa7fd31008a744c3c99f717e7d16d81d371d12",
|
||||
{{{0x1e40368, HostBridgeKind::kAllocate, "small-allocate"},
|
||||
{0x1e436b0, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x1e5977c, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1e4007c, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x648d13c, HostBridgeKind::kAlignedAllocate, "aligned-allocate-direct"},
|
||||
{0x1e43de4, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0, 0, 0x71a32a8, 0x400, 0x1e406f0, 0x1eaa240,
|
||||
0x71b21b0, 0x71b2240, 0x400000},
|
||||
{0x1e4007c, 0x1e43de4},
|
||||
0x69dcab0, 3673,
|
||||
{{{5, 6}, {7, 3673}}}, 2,
|
||||
{{{4, 3673}}}, 1, 7,
|
||||
{0x21aeb44, 0x70e60f0},
|
||||
HostAllocatorStrategy::kNativeMimalloc},
|
||||
// Roblox 2.738.1397; promoted from the exact-build probation profile.
|
||||
{"5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
|
||||
{{{0x1d73422, HostBridgeKind::kAllocate, "small-allocate"},
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
@@ -21,16 +23,24 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/graphics/vulkan_barrier_adjustment.h"
|
||||
#include "mocktail/graphics/vulkan_foveation_device_policy.h"
|
||||
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
|
||||
#include "mocktail/graphics/chrome_trace_writer.h"
|
||||
#include "mocktail/graphics/present_mode_policy.h"
|
||||
#include "mocktail/graphics/vulkan_etc2_emulation.h"
|
||||
#include "mocktail/graphics/vulkan_text_overlay_compositor.h"
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
#include "mocktail/vr/foveation_focus.h"
|
||||
#include "mocktail/vr/foveation_msaa_contract.h"
|
||||
#include "mocktail/vr/foveation_tile_bounds.h"
|
||||
#include "mocktail/vr/fsr3_scene_inputs.h"
|
||||
|
||||
extern "C" {
|
||||
VKAPI_ATTR VkResult VKAPI_CALL
|
||||
mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device);
|
||||
VKAPI_ATTR std::int32_t mocktail_vulkan_run_queue_synchronized(
|
||||
VkDevice device, VkQueue queue, std::int32_t (*operation)(void *), void *context);
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
|
||||
PFN_vkQueueSubmit raw, VkDevice device, VkQueue queue, uint32_t count,
|
||||
const VkSubmitInfo *submits, VkFence fence);
|
||||
@@ -178,9 +188,27 @@ VKAPI_ATTR void VKAPI_CALL
|
||||
vkCmdBeginRenderPass(VkCommandBuffer command_buffer,
|
||||
const VkRenderPassBeginInfo* begin_info,
|
||||
VkSubpassContents contents);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass(VkCommandBuffer command_buffer);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline(VkCommandBuffer,VkPipelineBindPoint,VkPipeline);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDraw(VkCommandBuffer,uint32_t,uint32_t,uint32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed(VkCommandBuffer,uint32_t,uint32_t,uint32_t,int32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirect(VkCommandBuffer,VkBuffer,VkDeviceSize,uint32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirect(VkCommandBuffer,VkBuffer,VkDeviceSize,uint32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectCount(VkCommandBuffer,VkBuffer,VkDeviceSize,VkBuffer,VkDeviceSize,uint32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirectCount(VkCommandBuffer,VkBuffer,VkDeviceSize,VkBuffer,VkDeviceSize,uint32_t,uint32_t);
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyPipeline(VkDevice,VkPipeline,const VkAllocationCallbacks*);
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyShaderModule(VkDevice,VkShaderModule,const VkAllocationCallbacks*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool FovTraceEnabled();
|
||||
bool Fsr3InputsAvailable();
|
||||
bool Fsr3InputsEnabled();
|
||||
bool Fsr3ShaderDumpEnabled();
|
||||
void Fsr3DumpShader(VkDevice,VkShaderModule,const VkShaderModuleCreateInfo*);
|
||||
void Fsr3DumpGraphicsPipelines(VkDevice,unsigned,const VkGraphicsPipelineCreateInfo*,const VkPipeline*);
|
||||
void Fsr3ResetCommand(VkCommandBuffer);
|
||||
void FovTraceComplete(VkDevice,VkQueue);
|
||||
|
||||
using BackendWindowFn = void* (*)();
|
||||
using UsesDirectVulkanFn = bool (*)();
|
||||
@@ -195,7 +223,7 @@ using VrXrCreateDeviceFn = bool (*)(VkPhysicalDevice,
|
||||
const VkDeviceCreateInfo*,
|
||||
const VkAllocationCallbacks*, VkDevice*,
|
||||
VkResult*);
|
||||
using VrXrNoteDeviceDestroyedFn = void (*)(VkDevice);
|
||||
using VrXrNoteDeviceDestroyedFn = bool (*)(VkDevice);
|
||||
using VrXrNoteQueueFn = void (*)(VkDevice, VkQueue, std::uint32_t,
|
||||
std::uint32_t);
|
||||
using VrXrRecordImageFn = void (*)(VkDevice, VkImage,
|
||||
@@ -249,7 +277,20 @@ struct AdapterState {
|
||||
PFN_vkDestroyFramebuffer destroy_framebuffer = nullptr;
|
||||
PFN_vkCreateRenderPass create_render_pass = nullptr;
|
||||
PFN_vkDestroyRenderPass destroy_render_pass = nullptr;
|
||||
PFN_vkCmdPipelineBarrier cmd_pipeline_barrier = nullptr;
|
||||
PFN_vkCmdPipelineBarrier2 cmd_pipeline_barrier2 = nullptr;
|
||||
PFN_vkCmdPipelineBarrier2KHR cmd_pipeline_barrier2_khr = nullptr;
|
||||
PFN_vkCmdBeginRenderPass cmd_begin_render_pass = nullptr;
|
||||
PFN_vkCmdEndRenderPass cmd_end_render_pass = nullptr;
|
||||
PFN_vkCmdBindPipeline cmd_bind_pipeline = nullptr;
|
||||
PFN_vkCmdDraw cmd_draw = nullptr;
|
||||
PFN_vkCmdDrawIndexed cmd_draw_indexed = nullptr;
|
||||
PFN_vkCmdDrawIndirect cmd_draw_indirect = nullptr;
|
||||
PFN_vkCmdDrawIndexedIndirect cmd_draw_indexed_indirect = nullptr;
|
||||
PFN_vkCmdDrawIndirectCount cmd_draw_indirect_count = nullptr;
|
||||
PFN_vkCmdDrawIndexedIndirectCount cmd_draw_indexed_indirect_count = nullptr;
|
||||
PFN_vkDestroyPipeline destroy_pipeline = nullptr;
|
||||
PFN_vkDestroyShaderModule destroy_shader_module = nullptr;
|
||||
};
|
||||
|
||||
struct QueueBinding {
|
||||
@@ -424,11 +465,20 @@ bool EnsureInitialized() {
|
||||
VkFence))>("mocktail_vr_xr_queue_submit_adapter");
|
||||
if (register_submit)
|
||||
register_submit(mocktail_vulkan_submit_synchronized);
|
||||
const auto register_operation = ResolveProcessFunction<void (*)(
|
||||
std::int32_t (*)(VkDevice, VkQueue, std::int32_t (*)(void *), void *))>(
|
||||
"mocktail_vr_xr_queue_operation_adapter");
|
||||
if (register_operation)
|
||||
register_operation(mocktail_vulkan_run_queue_synchronized);
|
||||
const auto register_idle = ResolveProcessFunction<void (*)(
|
||||
VkResult(VKAPI_PTR *)(PFN_vkDeviceWaitIdle, VkDevice))>(
|
||||
"mocktail_vr_xr_device_idle_adapter");
|
||||
if (register_idle)
|
||||
register_idle(mocktail_vulkan_idle_synchronized);
|
||||
if(FovTraceEnabled()) {
|
||||
const auto report=ResolveProcessFunction<void(*)(void(*)(VkDevice,VkQueue))>("mocktail_vr_xr_pass_timing_reporter");
|
||||
if(report)report(FovTraceComplete);
|
||||
}
|
||||
|
||||
const BackendWindowFn backend_window =
|
||||
ResolveProcessFunction<BackendWindowFn>("mocktail_window_backend_window");
|
||||
@@ -809,8 +859,43 @@ void RegisterHostDeviceDispatch(VkDevice device,
|
||||
get_device_proc_addr(device, "vkCreateRenderPass"));
|
||||
dispatch.destroy_render_pass = reinterpret_cast<PFN_vkDestroyRenderPass>(
|
||||
get_device_proc_addr(device, "vkDestroyRenderPass"));
|
||||
dispatch.cmd_pipeline_barrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(
|
||||
get_device_proc_addr(device, "vkCmdPipelineBarrier"));
|
||||
dispatch.cmd_pipeline_barrier2 = reinterpret_cast<PFN_vkCmdPipelineBarrier2>(
|
||||
get_device_proc_addr(device, "vkCmdPipelineBarrier2"));
|
||||
dispatch.cmd_pipeline_barrier2_khr = reinterpret_cast<PFN_vkCmdPipelineBarrier2KHR>(
|
||||
get_device_proc_addr(device, "vkCmdPipelineBarrier2KHR"));
|
||||
dispatch.cmd_begin_render_pass = reinterpret_cast<PFN_vkCmdBeginRenderPass>(
|
||||
get_device_proc_addr(device, "vkCmdBeginRenderPass"));
|
||||
dispatch.cmd_end_render_pass = reinterpret_cast<PFN_vkCmdEndRenderPass>(
|
||||
get_device_proc_addr(device, "vkCmdEndRenderPass"));
|
||||
dispatch.cmd_bind_pipeline = reinterpret_cast<PFN_vkCmdBindPipeline>(
|
||||
get_device_proc_addr(device, "vkCmdBindPipeline"));
|
||||
dispatch.cmd_draw = reinterpret_cast<PFN_vkCmdDraw>(
|
||||
get_device_proc_addr(device, "vkCmdDraw"));
|
||||
dispatch.cmd_draw_indexed = reinterpret_cast<PFN_vkCmdDrawIndexed>(
|
||||
get_device_proc_addr(device, "vkCmdDrawIndexed"));
|
||||
dispatch.cmd_draw_indirect = reinterpret_cast<PFN_vkCmdDrawIndirect>(
|
||||
get_device_proc_addr(device, "vkCmdDrawIndirect"));
|
||||
dispatch.cmd_draw_indexed_indirect = reinterpret_cast<PFN_vkCmdDrawIndexedIndirect>(
|
||||
get_device_proc_addr(device, "vkCmdDrawIndexedIndirect"));
|
||||
dispatch.cmd_draw_indirect_count = reinterpret_cast<PFN_vkCmdDrawIndirectCount>(
|
||||
get_device_proc_addr(device, "vkCmdDrawIndirectCount"));
|
||||
dispatch.cmd_draw_indexed_indirect_count = reinterpret_cast<PFN_vkCmdDrawIndexedIndirectCount>(
|
||||
get_device_proc_addr(device, "vkCmdDrawIndexedIndirectCount"));
|
||||
dispatch.destroy_pipeline = reinterpret_cast<PFN_vkDestroyPipeline>(
|
||||
get_device_proc_addr(device, "vkDestroyPipeline"));
|
||||
dispatch.destroy_shader_module = reinterpret_cast<PFN_vkDestroyShaderModule>(
|
||||
get_device_proc_addr(device, "vkDestroyShaderModule"));
|
||||
if(!dispatch.cmd_draw_indirect_count)dispatch.cmd_draw_indirect_count=reinterpret_cast<PFN_vkCmdDrawIndirectCount>(
|
||||
get_device_proc_addr(device,"vkCmdDrawIndirectCountKHR"));
|
||||
if(!dispatch.cmd_draw_indirect_count)dispatch.cmd_draw_indirect_count=reinterpret_cast<PFN_vkCmdDrawIndirectCount>(
|
||||
get_device_proc_addr(device,"vkCmdDrawIndirectCountAMD"));
|
||||
if(!dispatch.cmd_draw_indexed_indirect_count)dispatch.cmd_draw_indexed_indirect_count=reinterpret_cast<PFN_vkCmdDrawIndexedIndirectCount>(
|
||||
get_device_proc_addr(device,"vkCmdDrawIndexedIndirectCountKHR"));
|
||||
if(!dispatch.cmd_draw_indexed_indirect_count)dispatch.cmd_draw_indexed_indirect_count=reinterpret_cast<PFN_vkCmdDrawIndexedIndirectCount>(
|
||||
get_device_proc_addr(device,"vkCmdDrawIndexedIndirectCountAMD"));
|
||||
|
||||
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
@@ -1068,6 +1153,18 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
MOCKTAIL_VK_PROC(vkCreateRenderPass)
|
||||
MOCKTAIL_VK_PROC(vkDestroyRenderPass)
|
||||
MOCKTAIL_VK_PROC(vkCmdBeginRenderPass)
|
||||
if (FovTraceEnabled() || Fsr3InputsAvailable()) { MOCKTAIL_VK_PROC(vkCmdEndRenderPass) }
|
||||
if (Fsr3InputsAvailable()) {
|
||||
MOCKTAIL_VK_PROC(vkCmdBindPipeline)
|
||||
MOCKTAIL_VK_PROC(vkCmdDraw)
|
||||
MOCKTAIL_VK_PROC(vkCmdDrawIndexed)
|
||||
MOCKTAIL_VK_PROC(vkCmdDrawIndirect)
|
||||
MOCKTAIL_VK_PROC(vkCmdDrawIndexedIndirect)
|
||||
MOCKTAIL_VK_PROC(vkCmdDrawIndirectCount)
|
||||
MOCKTAIL_VK_PROC(vkCmdDrawIndexedIndirectCount)
|
||||
MOCKTAIL_VK_PROC(vkDestroyPipeline)
|
||||
MOCKTAIL_VK_PROC(vkDestroyShaderModule)
|
||||
}
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory)
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_PROC(vkMapMemory)
|
||||
@@ -1094,6 +1191,12 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
vkGetPhysicalDeviceImageFormatProperties2)
|
||||
MOCKTAIL_VK_ALIAS(vkBindBufferMemory2KHR, vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyImage2KHR, vkCmdCopyImage2)
|
||||
if(Fsr3InputsAvailable()) {
|
||||
MOCKTAIL_VK_ALIAS(vkCmdDrawIndirectCountKHR,vkCmdDrawIndirectCount)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdDrawIndexedIndirectCountKHR,vkCmdDrawIndexedIndirectCount)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdDrawIndirectCountAMD,vkCmdDrawIndirectCount)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdDrawIndexedIndirectCountAMD,vkCmdDrawIndexedIndirectCount)
|
||||
}
|
||||
MOCKTAIL_VK_ALIAS(vkMapMemory2KHR, vkMapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkUnmapMemory2KHR, vkUnmapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyBufferToImage2KHR, vkCmdCopyBufferToImage2)
|
||||
@@ -1181,7 +1284,21 @@ bool IsDeviceAdapterProc(const char* name) {
|
||||
std::strcmp(name, "vkDestroyFramebuffer") == 0 ||
|
||||
std::strcmp(name, "vkCreateRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkDestroyRenderPass") == 0 ||
|
||||
std::strcmp(name, "vkCmdBeginRenderPass") == 0);
|
||||
std::strcmp(name, "vkCmdBeginRenderPass") == 0 ||
|
||||
(Fsr3InputsAvailable() && std::strcmp(name,"vkCmdBindPipeline")==0) ||
|
||||
(Fsr3InputsAvailable() && (std::strcmp(name,"vkCmdDraw")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndexed")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndirect")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndexedIndirect")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndirectCount")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndexedIndirectCount")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndirectCountKHR")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndexedIndirectCountKHR")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndirectCountAMD")==0 ||
|
||||
std::strcmp(name,"vkCmdDrawIndexedIndirectCountAMD")==0)) ||
|
||||
(Fsr3InputsAvailable() && (std::strcmp(name,"vkDestroyPipeline")==0 ||
|
||||
std::strcmp(name,"vkDestroyShaderModule")==0)) ||
|
||||
((FovTraceEnabled() || Fsr3InputsAvailable()) && std::strcmp(name, "vkCmdEndRenderPass") == 0));
|
||||
}
|
||||
|
||||
bool IsGlobalAdapterProc(const char* name) {
|
||||
@@ -1557,6 +1674,7 @@ VkResult VKAPI_CALL ProfiledCreateGraphicsPipelines(
|
||||
"pipeline");
|
||||
const VkResult result =
|
||||
host(device, cache, count, infos, allocator, pipelines);
|
||||
if(result==VK_SUCCESS)Fsr3DumpGraphicsPipelines(device,count,infos,pipelines);
|
||||
scope.Arg("count", count);
|
||||
scope.Arg("cache", cache != VK_NULL_HANDLE);
|
||||
scope.Arg("result", result);
|
||||
@@ -1594,6 +1712,7 @@ VkResult VKAPI_CALL ProfiledCreateShaderModule(
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateShaderModule", "pipeline");
|
||||
const VkResult result = host(device, create_info, allocator, shader_module);
|
||||
if(result==VK_SUCCESS && shader_module)Fsr3DumpShader(device,*shader_module,create_info);
|
||||
scope.Arg("bytes", create_info != nullptr
|
||||
? static_cast<std::int64_t>(create_info->codeSize)
|
||||
: 0);
|
||||
@@ -1647,9 +1766,12 @@ VkResult VKAPI_CALL ProfiledAllocateMemory(
|
||||
// These take precedence over the ETC2 wrappers, so a command both wrap is
|
||||
// timed inside its adapter function instead.
|
||||
PFN_vkVoidFunction ProfileAdapterProc(const char* name) {
|
||||
if (name == nullptr || mocktail::graphics::ActiveProfileTrace() == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (name == nullptr) return nullptr;
|
||||
if (std::strcmp(name, "vkCreateShaderModule") == 0 && (Fsr3ShaderDumpEnabled() || Fsr3InputsAvailable()))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledCreateShaderModule);
|
||||
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0 && (Fsr3ShaderDumpEnabled() || Fsr3InputsAvailable()))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledCreateGraphicsPipelines);
|
||||
if (mocktail::graphics::ActiveProfileTrace() == nullptr) return nullptr;
|
||||
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(
|
||||
ProfiledCreateGraphicsPipelines);
|
||||
@@ -1705,6 +1827,7 @@ void ReleaseEtc2PoolCommandBuffers(VkDevice device,
|
||||
}
|
||||
for (VkCommandBuffer command_buffer : command_buffers) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
Fsr3ResetCommand(command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1838,6 +1961,34 @@ const VkSubmitInfo2* PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
return rewritten_any ? rewrite->rewritten2.data() : submits;
|
||||
}
|
||||
|
||||
#include "vulkan_foveation_adapter.inc"
|
||||
#include "vulkan_temporal_inputs_adapter.inc"
|
||||
|
||||
using DesktopQueueNormalizer = VkImageLayout (*)(VkCommandBuffer, VkImage,
|
||||
unsigned*, unsigned*, VkImageLayout*, VkImageLayout);
|
||||
DesktopQueueNormalizer DesktopBarrierNormalizer() {
|
||||
static const auto normalize = ResolveProcessFunction<DesktopQueueNormalizer>(
|
||||
"mocktail_vr_xr_desktop_barrier");
|
||||
return normalize;
|
||||
}
|
||||
|
||||
template <class Barrier>
|
||||
static void NormalizeDesktopQueueBarriers(VkCommandBuffer command,
|
||||
std::uint32_t count,
|
||||
Barrier* barriers) {
|
||||
const auto normalize = DesktopBarrierNormalizer();
|
||||
if (!normalize) return;
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
auto& image = barriers[i];
|
||||
if (mocktail::graphics::internal::NeedsDesktopQueueNormalization(
|
||||
image.srcQueueFamilyIndex, image.dstQueueFamilyIndex))
|
||||
image.newLayout = normalize(command, image.image,
|
||||
&image.srcQueueFamilyIndex, &image.dstQueueFamilyIndex,
|
||||
&image.oldLayout, image.newLayout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -1933,11 +2084,11 @@ vkCreateInstance(const VkInstanceCreateInfo* create_info,
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL
|
||||
vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks* allocator) {
|
||||
const auto note = ResolveProcessFunction<void (*)(VkInstance)>(
|
||||
const auto note = ResolveProcessFunction<bool (*)(VkInstance)>(
|
||||
"mocktail_vr_xr_note_instance_destroyed");
|
||||
if (note != nullptr) note(instance);
|
||||
const bool retained_by_backend = note != nullptr && note(instance);
|
||||
|
||||
const auto host_destroy = reinterpret_cast<PFN_vkDestroyInstance>(
|
||||
const auto host_destroy = retained_by_backend ? nullptr : reinterpret_cast<PFN_vkDestroyInstance>(
|
||||
HostInstanceProc(instance, "vkDestroyInstance"));
|
||||
if (host_destroy != nullptr) {
|
||||
host_destroy(instance, allocator);
|
||||
@@ -2200,6 +2351,32 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
}
|
||||
}
|
||||
|
||||
// Opt-in shader-work counters for real guest render passes. Preserve the
|
||||
// caller's core feature record after the synchronous device-creation call.
|
||||
bool trace_statistics=false;
|
||||
auto* statistics_features2=const_cast<VkPhysicalDeviceFeatures2*>(
|
||||
reinterpret_cast<const VkPhysicalDeviceFeatures2*>(FindFeature(
|
||||
host_info.pNext,VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)));
|
||||
const VkBool32 original_statistics=statistics_features2 ?
|
||||
statistics_features2->features.pipelineStatisticsQuery : VK_FALSE;
|
||||
if(FovTraceEnabled()) {
|
||||
VkPhysicalDeviceFeatures supported{};
|
||||
const auto get_features=reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
HostInstanceProc(instance,"vkGetPhysicalDeviceFeatures"));
|
||||
if(get_features)get_features(physical_device,&supported);
|
||||
trace_statistics=supported.pipelineStatisticsQuery==VK_TRUE;
|
||||
if(trace_statistics) {
|
||||
if(statistics_features2)statistics_features2->features.pipelineStatisticsQuery=VK_TRUE;
|
||||
else {
|
||||
if(host_info.pEnabledFeatures)host_enabled_features=*host_info.pEnabledFeatures;
|
||||
host_enabled_features.pipelineStatisticsQuery=VK_TRUE;
|
||||
host_info.pEnabledFeatures=&host_enabled_features;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr," [vr-pass-gpu] fragment shader counters %s\n",trace_statistics?"enabled":"unsupported");
|
||||
}
|
||||
const auto foveation_policy =
|
||||
mocktail::graphics::internal::SnapshotFoveationDevicePolicy(&host_info);
|
||||
VkResult result = VK_ERROR_INITIALIZATION_FAILED;
|
||||
bool vr_handled = false;
|
||||
const auto vr_create_device =
|
||||
@@ -2214,6 +2391,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
if (!vr_handled) {
|
||||
result = host_create(physical_device, &host_info, allocator, device);
|
||||
}
|
||||
if(trace_statistics && statistics_features2)statistics_features2->features.pipelineStatisticsQuery=original_statistics;
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2->features.textureCompressionETC2 =
|
||||
requested_features2_etc2;
|
||||
@@ -2266,6 +2444,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
break;
|
||||
}
|
||||
|
||||
FovRegisterDevice(*device, physical_device, memory_properties, graphics_family, trace_statistics, foveation_policy);
|
||||
|
||||
const bool registered =
|
||||
enabled.placebo_required && graphics_family != UINT32_MAX &&
|
||||
State().text_overlay.RegisterDevice(
|
||||
@@ -2292,14 +2472,15 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) {
|
||||
HostDeviceProc(device, "vkDestroyDevice"));
|
||||
const auto vr_note_device_destroyed =
|
||||
State().vr_note_device_destroyed.load(std::memory_order_acquire);
|
||||
if (vr_note_device_destroyed != nullptr) {
|
||||
// Runs while the device is still valid so the XR backend can destroy
|
||||
// its session and swapchain resources first.
|
||||
vr_note_device_destroyed(device);
|
||||
}
|
||||
// Guest helper resources must always be released, even when the native
|
||||
// device stays alive for the next renderer and the existing XR session.
|
||||
Fsr3ForgetDevice(device);
|
||||
FovDestroyDevice(device);
|
||||
const bool retained = vr_note_device_destroyed != nullptr &&
|
||||
vr_note_device_destroyed(device);
|
||||
State().text_overlay.DestroyDevice(device);
|
||||
State().etc2.DestroyDevice(device);
|
||||
if (host_destroy != nullptr) {
|
||||
if (!retained && host_destroy != nullptr) {
|
||||
host_destroy(device, allocator);
|
||||
}
|
||||
RemoveHostDeviceDispatch(device);
|
||||
@@ -2677,6 +2858,7 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
|
||||
if (command_buffers != nullptr) {
|
||||
for (std::uint32_t index = 0; index < command_buffer_count; ++index) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffers[index]);
|
||||
Fsr3ResetCommand(command_buffers[index]);
|
||||
}
|
||||
}
|
||||
static const auto note = ResolveProcessFunction<void (*)(
|
||||
@@ -2722,7 +2904,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
Fsr3ResetCommand(command_buffer);
|
||||
const VkResult result = host_begin(command_buffer, begin_info);
|
||||
if(result==VK_SUCCESS)FovTraceReset(command_buffer);
|
||||
static const auto note = ResolveProcessFunction<void (*)(VkCommandBuffer)>(
|
||||
"mocktail_vr_xr_reset_command");
|
||||
if (result == VK_SUCCESS && note)
|
||||
@@ -2755,6 +2939,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
Fsr3ResetCommand(command_buffer);
|
||||
const VkResult result = host_reset(command_buffer, flags);
|
||||
static const auto note = ResolveProcessFunction<void (*)(VkCommandBuffer)>(
|
||||
"mocktail_vr_xr_reset_command");
|
||||
@@ -2803,8 +2988,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
PrepareEtc2Submit(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, prepared, fence, LockedHostSubmit);
|
||||
if (result == VK_SUCCESS)
|
||||
if (result == VK_SUCCESS) {
|
||||
NoteVrSubmitted(queue, submit_count, prepared);
|
||||
FovTraceSubmitted(queue,submit_count,prepared);
|
||||
}
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2827,8 +3014,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
PrepareEtc2Submit2(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, prepared, fence, LockedHostSubmit2);
|
||||
if (result == VK_SUCCESS)
|
||||
if (result == VK_SUCCESS) {
|
||||
NoteVrSubmitted2(queue, submit_count, prepared);
|
||||
FovTraceSubmitted(queue,submit_count,prepared);
|
||||
}
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2851,8 +3040,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
PrepareEtc2Submit2(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, prepared, fence, LockedHostSubmit2KHR);
|
||||
if (result == VK_SUCCESS)
|
||||
if (result == VK_SUCCESS) {
|
||||
NoteVrSubmitted2(queue, submit_count, prepared);
|
||||
FovTraceSubmitted(queue,submit_count,prepared);
|
||||
}
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -3218,10 +3409,14 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
|
||||
const VkAllocationCallbacks* allocator, VkImage* image) {
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateImage", "memory");
|
||||
VkImageCreateInfo effective{};
|
||||
const VkResult result =
|
||||
State().etc2.CreateImage(device, create_info, allocator, image);
|
||||
Fsr3CreateImage(device, create_info, allocator, image, &effective);
|
||||
const auto record = State().vr_record_image.load(std::memory_order_acquire);
|
||||
if (result == VK_SUCCESS && record != nullptr && image != nullptr) {
|
||||
// Preserve the guest's original provenance contract. Adding transfer
|
||||
// usage for temporal capture must not make a scene intermediate appear
|
||||
// to the existing backend as a new final-eye target.
|
||||
record(device, *image, create_info);
|
||||
}
|
||||
return result;
|
||||
@@ -3235,6 +3430,7 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyImage(
|
||||
if (note != nullptr && image != VK_NULL_HANDLE) {
|
||||
note(image);
|
||||
}
|
||||
Fsr3ForgetImage(image);
|
||||
State().etc2.DestroyImage(device, image, allocator);
|
||||
}
|
||||
|
||||
@@ -3249,6 +3445,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
|
||||
view != nullptr) {
|
||||
record(*view, create_info->image);
|
||||
}
|
||||
if(result==VK_SUCCESS && view)Fsr3RecordView(*view,create_info);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3359,6 +3556,7 @@ vkDestroyImageView(VkDevice device, VkImageView image_view,
|
||||
if (note != nullptr && image_view != VK_NULL_HANDLE) {
|
||||
note(image_view);
|
||||
}
|
||||
Fsr3ForgetView(image_view);
|
||||
const PFN_vkDestroyImageView host_destroy =
|
||||
HostDispatchForDevice(device).destroy_image_view;
|
||||
if (host_destroy != nullptr) {
|
||||
@@ -3374,18 +3572,21 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateFramebuffer(
|
||||
if (host_create == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = host_create(device, create_info, allocator, framebuffer);
|
||||
const VkResult result = FovCreateFramebuffer(device, create_info, allocator, framebuffer);
|
||||
const auto record =
|
||||
State().vr_record_framebuffer.load(std::memory_order_acquire);
|
||||
if (result == VK_SUCCESS && record != nullptr) {
|
||||
record(*framebuffer, create_info);
|
||||
}
|
||||
if(result==VK_SUCCESS && framebuffer)Fsr3RecordFramebuffer(device,*framebuffer,create_info);
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL
|
||||
vkDestroyFramebuffer(VkDevice device, VkFramebuffer framebuffer,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
Fsr3ForgetFramebuffer(framebuffer);
|
||||
FovForgetFramebuffer(device, framebuffer);
|
||||
const auto note =
|
||||
State().vr_note_destroy_framebuffer.load(std::memory_order_acquire);
|
||||
if (note != nullptr && framebuffer != VK_NULL_HANDLE) {
|
||||
@@ -3406,7 +3607,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass(
|
||||
if (host_create == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
const VkResult result = host_create(device, create_info, allocator, render_pass);
|
||||
const VkResult result = Fsr3CreateRenderPass(device, create_info, allocator, render_pass);
|
||||
if(result==VK_SUCCESS && render_pass)Fsr3RecordDiagnosticPass(*render_pass,create_info);
|
||||
const auto record =
|
||||
State().vr_record_render_pass.load(std::memory_order_acquire);
|
||||
if (result == VK_SUCCESS && record != nullptr) {
|
||||
@@ -3418,6 +3620,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass(
|
||||
VKAPI_ATTR void VKAPI_CALL
|
||||
vkDestroyRenderPass(VkDevice device, VkRenderPass render_pass,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
Fsr3ForgetPass(render_pass);
|
||||
FovForgetRenderPass(render_pass);
|
||||
const PFN_vkDestroyRenderPass host_destroy =
|
||||
HostDispatchForDevice(device).destroy_render_pass;
|
||||
if (host_destroy != nullptr) {
|
||||
@@ -3437,10 +3641,80 @@ vkCmdBeginRenderPass(VkCommandBuffer command_buffer,
|
||||
const PFN_vkCmdBeginRenderPass host_begin =
|
||||
HostDispatchForCommandBuffer(command_buffer).cmd_begin_render_pass;
|
||||
if (host_begin != nullptr) {
|
||||
host_begin(command_buffer, begin_info, contents);
|
||||
if (begin_info) {
|
||||
auto adjusted = *begin_info;
|
||||
adjusted.framebuffer = FovFramebufferForBegin(HostDispatchForCommandBuffer(command_buffer).device, begin_info);
|
||||
FovTraceBegin(command_buffer,HostDispatchForCommandBuffer(command_buffer).device,begin_info,adjusted.framebuffer!=begin_info->framebuffer,contents==VK_SUBPASS_CONTENTS_INLINE);
|
||||
Fsr3SceneBegin(HostDispatchForCommandBuffer(command_buffer).device,command_buffer,begin_info,contents);
|
||||
adjusted.renderPass=Fsr3RenderPassForBegin(HostDispatchForCommandBuffer(command_buffer).device,begin_info->renderPass);
|
||||
host_begin(command_buffer, &adjusted, contents);
|
||||
FovTraceDrawScope(command_buffer,HostDispatchForCommandBuffer(command_buffer).device,true);
|
||||
} else {
|
||||
host_begin(command_buffer, begin_info, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDraw(VkCommandBuffer command,uint32_t vertex_count,
|
||||
uint32_t instance_count,uint32_t first_vertex,uint32_t first_instance) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw;
|
||||
if(host){Fsr3NoteDraw(command);host(command,vertex_count,instance_count,first_vertex,first_instance);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed(VkCommandBuffer command,uint32_t count,
|
||||
uint32_t instances,uint32_t first,int32_t offset,uint32_t first_instance) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw_indexed;
|
||||
if(host){Fsr3NoteDraw(command);host(command,count,instances,first,offset,first_instance);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirect(VkCommandBuffer command,VkBuffer buffer,
|
||||
VkDeviceSize offset,uint32_t count,uint32_t stride) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw_indirect;
|
||||
if(host){Fsr3NoteDraw(command);host(command,buffer,offset,count,stride);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirect(VkCommandBuffer command,VkBuffer buffer,
|
||||
VkDeviceSize offset,uint32_t count,uint32_t stride) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw_indexed_indirect;
|
||||
if(host){Fsr3NoteDraw(command);host(command,buffer,offset,count,stride);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectCount(VkCommandBuffer command,VkBuffer buffer,
|
||||
VkDeviceSize offset,VkBuffer count_buffer,VkDeviceSize count_offset,uint32_t max_count,uint32_t stride) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw_indirect_count;
|
||||
if(host){Fsr3NoteDraw(command);host(command,buffer,offset,count_buffer,count_offset,max_count,stride);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirectCount(VkCommandBuffer command,VkBuffer buffer,
|
||||
VkDeviceSize offset,VkBuffer count_buffer,VkDeviceSize count_offset,uint32_t max_count,uint32_t stride) {
|
||||
const auto host=HostDispatchForCommandBuffer(command).cmd_draw_indexed_indirect_count;
|
||||
if(host){Fsr3NoteDraw(command);host(command,buffer,offset,count_buffer,count_offset,max_count,stride);}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline(VkCommandBuffer command,
|
||||
VkPipelineBindPoint point,VkPipeline pipeline) {
|
||||
const auto& dispatch=HostDispatchForCommandBuffer(command);
|
||||
const auto host=dispatch.cmd_bind_pipeline;
|
||||
if(host){host(command,point,pipeline);Fsr3NotePipeline(dispatch.device,command,point,pipeline);}
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyPipeline(VkDevice device,VkPipeline pipeline,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
Fsr3ForgetPipeline(pipeline);
|
||||
const auto host=HostDispatchForDevice(device).destroy_pipeline;
|
||||
if(host)host(device,pipeline,allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyShaderModule(VkDevice device,VkShaderModule shader,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
Fsr3ForgetShader(shader);
|
||||
const auto destroy=HostDispatchForDevice(device).destroy_shader_module;
|
||||
if(destroy)destroy(device,shader,allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass(VkCommandBuffer command_buffer) {
|
||||
const auto& dispatch=HostDispatchForCommandBuffer(command_buffer);
|
||||
const auto device=dispatch.device;
|
||||
const auto end=dispatch.cmd_end_render_pass;
|
||||
if(end) {FovTraceDrawScope(command_buffer,device,false);end(command_buffer);Fsr3SceneEnd(device,command_buffer);FovTraceEnd(command_buffer,device);}
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(
|
||||
VkDevice device, const VkCommandPoolCreateInfo *info,
|
||||
const VkAllocationCallbacks *allocator, VkCommandPool *pool) {
|
||||
@@ -3457,63 +3731,73 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(
|
||||
VkCommandBuffer command, VkPipelineStageFlags src, VkPipelineStageFlags dst,
|
||||
VkDependencyFlags flags, uint32_t memory_count,
|
||||
const VkMemoryBarrier *memory, uint32_t buffer_count,
|
||||
const VkBufferMemoryBarrier *buffers, uint32_t image_count,
|
||||
const VkImageMemoryBarrier *images) {
|
||||
const auto barrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(
|
||||
HostDeviceProc(HostDispatchForCommandBuffer(command).device,
|
||||
"vkCmdPipelineBarrier"));
|
||||
if (!barrier)
|
||||
const auto barrier = HostDispatchForCommandBuffer(command).cmd_pipeline_barrier;
|
||||
if (!barrier) return;
|
||||
static const auto normalize_batch = ResolveProcessFunction<void (*)(
|
||||
VkCommandBuffer, unsigned, VkImageMemoryBarrier*)>(
|
||||
"mocktail_vr_xr_desktop_barriers");
|
||||
if (!normalize_batch && !DesktopBarrierNormalizer()) {
|
||||
barrier(command, src, dst, flags, memory_count, memory, buffer_count,
|
||||
buffers, image_count, images);
|
||||
return;
|
||||
static const auto normalize = ResolveProcessFunction<VkImageLayout (*)(
|
||||
VkCommandBuffer, VkImage, unsigned *, unsigned *, VkImageLayout *,
|
||||
VkImageLayout)>("mocktail_vr_xr_desktop_barrier");
|
||||
std::vector<VkImageMemoryBarrier> adjusted;
|
||||
if (normalize && images && image_count) {
|
||||
adjusted.assign(images, images + image_count);
|
||||
for (auto &image : adjusted)
|
||||
image.newLayout = normalize(
|
||||
command, image.image, &image.srcQueueFamilyIndex,
|
||||
&image.dstQueueFamilyIndex, &image.oldLayout, image.newLayout);
|
||||
images = adjusted.data();
|
||||
}
|
||||
barrier(command, src, dst, flags, memory_count, memory, buffer_count, buffers,
|
||||
image_count, images);
|
||||
mocktail::graphics::internal::WithDesktopQueueBarriers(image_count, images,
|
||||
[&](unsigned count, VkImageMemoryBarrier* adjusted) {
|
||||
if (normalize_batch) normalize_batch(command, count, adjusted);
|
||||
else NormalizeDesktopQueueBarriers(command, count, adjusted);
|
||||
}, [&](const VkImageMemoryBarrier* adjusted) {
|
||||
barrier(command, src, dst, flags, memory_count, memory, buffer_count,
|
||||
buffers, image_count, adjusted);
|
||||
});
|
||||
}
|
||||
|
||||
static void ForwardVrBarrier2(VkCommandBuffer command,
|
||||
const VkDependencyInfo *info, const char *name) {
|
||||
const auto barrier = reinterpret_cast<PFN_vkCmdPipelineBarrier2>(
|
||||
HostDeviceProc(HostDispatchForCommandBuffer(command).device, name));
|
||||
if (!barrier || !info)
|
||||
const VkDependencyInfo *info,
|
||||
PFN_vkCmdPipelineBarrier2 barrier) {
|
||||
if (!barrier || !info) return;
|
||||
static const auto normalize_batch = ResolveProcessFunction<void (*)(
|
||||
VkCommandBuffer, unsigned, VkImageMemoryBarrier2*)>(
|
||||
"mocktail_vr_xr_desktop_barriers2");
|
||||
if (!normalize_batch && !DesktopBarrierNormalizer()) {
|
||||
barrier(command, info);
|
||||
return;
|
||||
static const auto normalize = ResolveProcessFunction<VkImageLayout (*)(
|
||||
VkCommandBuffer, VkImage, unsigned *, unsigned *, VkImageLayout *,
|
||||
VkImageLayout)>("mocktail_vr_xr_desktop_barrier");
|
||||
VkDependencyInfo adjusted_info = *info;
|
||||
std::vector<VkImageMemoryBarrier2> adjusted;
|
||||
if (normalize && info->pImageMemoryBarriers &&
|
||||
info->imageMemoryBarrierCount) {
|
||||
adjusted.assign(info->pImageMemoryBarriers,
|
||||
info->pImageMemoryBarriers + info->imageMemoryBarrierCount);
|
||||
for (auto &image : adjusted)
|
||||
image.newLayout = normalize(
|
||||
command, image.image, &image.srcQueueFamilyIndex,
|
||||
&image.dstQueueFamilyIndex, &image.oldLayout, image.newLayout);
|
||||
adjusted_info.pImageMemoryBarriers = adjusted.data();
|
||||
}
|
||||
barrier(command, &adjusted_info);
|
||||
mocktail::graphics::internal::WithDesktopQueueBarriers(
|
||||
info->imageMemoryBarrierCount, info->pImageMemoryBarriers,
|
||||
[&](unsigned count, VkImageMemoryBarrier2* adjusted) {
|
||||
if (normalize_batch) normalize_batch(command, count, adjusted);
|
||||
else NormalizeDesktopQueueBarriers(command, count, adjusted);
|
||||
}, [&](const VkImageMemoryBarrier2* adjusted) {
|
||||
if (adjusted == info->pImageMemoryBarriers) barrier(command, info);
|
||||
else {
|
||||
auto adjusted_info = *info;
|
||||
adjusted_info.pImageMemoryBarriers = adjusted;
|
||||
barrier(command, &adjusted_info);
|
||||
}
|
||||
});
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2(VkCommandBuffer command,
|
||||
const VkDependencyInfo *info) {
|
||||
ForwardVrBarrier2(command, info, "vkCmdPipelineBarrier2");
|
||||
ForwardVrBarrier2(command, info,
|
||||
HostDispatchForCommandBuffer(command).cmd_pipeline_barrier2);
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2KHR(
|
||||
VkCommandBuffer command, const VkDependencyInfo *info) {
|
||||
ForwardVrBarrier2(command, info, "vkCmdPipelineBarrier2KHR");
|
||||
ForwardVrBarrier2(command, info,
|
||||
HostDispatchForCommandBuffer(command).cmd_pipeline_barrier2_khr);
|
||||
}
|
||||
|
||||
VKAPI_ATTR std::int32_t mocktail_vulkan_run_queue_synchronized(
|
||||
VkDevice device, VkQueue queue, std::int32_t (*operation)(void *), void *context) {
|
||||
const QueueCallGuard guard(device, queue);
|
||||
return operation(context);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mocktail_vulkan_submit_synchronized(
|
||||
@@ -3534,4 +3818,3 @@ mocktail_vulkan_idle_synchronized(PFN_vkDeviceWaitIdle raw, VkDevice device) {
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
// Experimental native FDM attachment bridge. Included in the loader adapter's
|
||||
// private namespace so all calls bypass guest interposition. Inert unless the
|
||||
// backend has opted in and successfully created runtime-owned density maps.
|
||||
struct FovRenderPass {
|
||||
VkDevice device; std::uint32_t attachments; unsigned samples=1; bool depth=false;
|
||||
unsigned subpasses=0,colors=0;VkFormat color_format=VK_FORMAT_UNDEFINED;
|
||||
VkFormat depth_format=VK_FORMAT_UNDEFINED;bool attached=false;
|
||||
mocktail::vr::FoveationTilePassIdentity tile_identity{};
|
||||
};
|
||||
struct FovFramebuffer {
|
||||
VkDevice device;
|
||||
VkFramebufferCreateInfo info;
|
||||
std::vector<VkImageView> attachments;
|
||||
struct Clone { VkImageView view; VkRenderPass pass; VkFramebuffer framebuffer; };
|
||||
std::vector<Clone> clones;
|
||||
};
|
||||
struct FovImage { VkImage image{}; VkDeviceMemory memory{}; VkImageView view{}; };
|
||||
struct FovDevice : FovImage {
|
||||
using FocusKey=mocktail::vr::internal::GazeDensityMapKey;
|
||||
FovImage eyes[2];
|
||||
VkBuffer staging{};VkDeviceMemory staging_memory{};void* mapped=nullptr;
|
||||
VkCommandPool pool{};VkCommandBuffer command{};VkFence fence{};VkQueue queue{};
|
||||
unsigned width=0,height=0;
|
||||
VkExtent2D min_density_region{},max_density_region{};
|
||||
bool framebuffer_density_mapping=false;
|
||||
mocktail::vr::FoveationTileDriverIdentity tile_driver{};
|
||||
std::string driver_name,driver_info;
|
||||
bool density_offsets_enabled=true;
|
||||
std::uint64_t frames[2]{};bool initialized[2]{};bool gaze_ready=false;
|
||||
bool host_copy=false;
|
||||
std::vector<std::uint8_t> uploaded[2],scratch_pixels[2];
|
||||
std::vector<std::uint16_t> scratch_densities[2];
|
||||
FocusKey focus_keys[2];bool focus_cached[2]{};
|
||||
float timestamp_period=0;
|
||||
unsigned timestamp_bits=0;bool statistics=false;
|
||||
};
|
||||
struct FovPassTiming {
|
||||
VkDevice device{};VkQueryPool pool{},statistics_pool{};VkFramebuffer framebuffer{};
|
||||
VkCommandBuffer command{};VkQueue submitted_queue{};bool ended=false;
|
||||
unsigned width=0,height=0,samples=1,visit=0;bool depth=false,foveated=false,reported=false;
|
||||
int eye=-1;void* owner=nullptr;void* guest_framebuffer=nullptr;
|
||||
VkFormat color_format=VK_FORMAT_UNDEFINED;
|
||||
};
|
||||
struct FovAdapterState {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<VkDevice,FovDevice> devices;
|
||||
std::unordered_map<VkRenderPass,FovRenderPass> passes;
|
||||
std::unordered_map<VkFramebuffer,FovFramebuffer> framebuffers;
|
||||
std::uint64_t selected = 0;
|
||||
std::uint64_t msaa_selected = 0;
|
||||
std::vector<FovPassTiming> timings;
|
||||
std::unordered_map<VkCommandBuffer,std::size_t> active_timings;
|
||||
std::unordered_map<VkFramebuffer,unsigned> timing_visits;
|
||||
};
|
||||
FovAdapterState& FovState() { static FovAdapterState state; return state; }
|
||||
template<class T> T FovProc(VkDevice device, const char* name) {
|
||||
return reinterpret_cast<T>(HostDeviceProc(device,name));
|
||||
}
|
||||
bool FovTraceEnabled() {
|
||||
static const bool enabled=[] {const char* value=std::getenv("MOCKTAIL_VR_TRACE_PASSES");return value&&value[0]=='1';}();
|
||||
return enabled;
|
||||
}
|
||||
bool FovSceneEnabled() {
|
||||
static const bool initial=[] {const char* value=std::getenv("MOCKTAIL_VR_SCENE_FOVEATION");return !value || std::strcmp(value,"0")!=0;}();
|
||||
static const bool benchmark=[] {const char* value=std::getenv("MOCKTAIL_VR_FOVEATION_BENCHMARK");return value && value[0]=='1';}();
|
||||
if(!FovTraceEnabled() || !benchmark)return initial;
|
||||
// Diagnostic-only A/B control in a single running scene; never read by the
|
||||
// normal launcher. Avoid comparing unrelated multiplayer server instances.
|
||||
static std::mutex mutex;std::lock_guard<std::mutex> lock(mutex);
|
||||
static bool value=initial;static std::uint64_t checked=0;
|
||||
const auto now=MonotonicNanos();
|
||||
if(now-checked>1000000000ull) {
|
||||
checked=now;char mode=0;std::ifstream file("/data/tools/scene-foveation-mode");
|
||||
if(file>>mode && (mode=='0' || mode=='1'))value=mode=='1';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// Called only after the existing eye-copy fence completed this queue. Never
|
||||
// read newly created/unsubmitted queries before their recorded reset ran.
|
||||
void FovTraceComplete(VkDevice device,VkQueue queue) {
|
||||
if(!FovTraceEnabled())return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto dev=state.devices.find(device);if(dev==state.devices.end())return;
|
||||
auto results=FovProc<PFN_vkGetQueryPoolResults>(device,"vkGetQueryPoolResults");
|
||||
for(auto& timing:state.timings)if(timing.device==device && timing.submitted_queue==queue && timing.ended && !timing.reported) {
|
||||
std::uint64_t values[4]{};
|
||||
const auto ready=results(device,timing.pool,0,2,sizeof(values),values,sizeof(std::uint64_t)*2,VK_QUERY_RESULT_64_BIT|VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if(ready==VK_SUCCESS && values[1] && values[3]) {
|
||||
const auto mask=dev->second.timestamp_bits>=64?UINT64_MAX:((1ull<<dev->second.timestamp_bits)-1);
|
||||
const double ms=double((values[2]-values[0])&mask)*dev->second.timestamp_period/1e6;
|
||||
std::fprintf(stderr," [vr-pass-gpu] framebuffer=%p extent=%ux%u samples=%u depth=%d format=%d eye=%d owner=%p guest_fb=%p foveated=%d visit=%u gpu_ms=%.3f\n",reinterpret_cast<void*>(timing.framebuffer),timing.width,timing.height,timing.samples,timing.depth,int(timing.color_format),timing.eye,timing.owner,timing.guest_framebuffer,timing.foveated,timing.visit,ms);
|
||||
if(timing.statistics_pool) {
|
||||
std::uint64_t statistics[2]{};
|
||||
const auto statistics_ready=results(device,timing.statistics_pool,0,1,sizeof(statistics),statistics,sizeof(statistics),VK_QUERY_RESULT_64_BIT|VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
if(statistics_ready==VK_SUCCESS && statistics[1])std::fprintf(stderr," [vr-pass-shading] framebuffer=%p extent=%ux%u depth=%d eye=%d foveated=%d visit=%u fragment_invocations=%llu\n",reinterpret_cast<void*>(timing.framebuffer),timing.width,timing.height,timing.depth,timing.eye,timing.foveated,timing.visit,static_cast<unsigned long long>(statistics[0]));
|
||||
}
|
||||
timing.reported=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
void FovTraceSubmittedCommand(VkQueue queue,VkCommandBuffer command) {
|
||||
if(!FovTraceEnabled())return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
for(auto& timing:state.timings)if(timing.command==command && timing.ended && !timing.submitted_queue && !timing.reported)timing.submitted_queue=queue;
|
||||
}
|
||||
void FovTraceReset(VkCommandBuffer command) {
|
||||
if(!FovTraceEnabled())return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
for(auto& timing:state.timings)if(timing.command==command && !timing.submitted_queue)timing.reported=true;
|
||||
state.active_timings.erase(command);
|
||||
}
|
||||
void FovTraceSubmitted(VkQueue queue,unsigned count,const VkSubmitInfo* submits) {
|
||||
if(!FovTraceEnabled() || !submits)return;
|
||||
for(unsigned i=0;i<count;++i)for(unsigned j=0;j<submits[i].commandBufferCount;++j)FovTraceSubmittedCommand(queue,submits[i].pCommandBuffers[j]);
|
||||
}
|
||||
void FovTraceSubmitted(VkQueue queue,unsigned count,const VkSubmitInfo2* submits) {
|
||||
if(!FovTraceEnabled() || !submits)return;
|
||||
for(unsigned i=0;i<count;++i)for(unsigned j=0;j<submits[i].commandBufferInfoCount;++j)FovTraceSubmittedCommand(queue,submits[i].pCommandBufferInfos[j].commandBuffer);
|
||||
}
|
||||
void FovTraceBegin(VkCommandBuffer command,VkDevice device,const VkRenderPassBeginInfo* begin,bool foveated,bool inline_draws) {
|
||||
if(!FovTraceEnabled() || !begin)return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto dev=state.devices.find(device);
|
||||
if(dev==state.devices.end() || !dev->second.timestamp_bits || dev->second.timestamp_period<=0)return;
|
||||
if(state.active_timings.count(command))return;
|
||||
const auto fb=state.framebuffers.find(begin->framebuffer);
|
||||
const auto pass=state.passes.find(begin->renderPass);
|
||||
if(fb==state.framebuffers.end() || pass==state.passes.end())return;
|
||||
static const bool scene_only=[] {const char* value=std::getenv("MOCKTAIL_VR_TRACE_SCENE_ONLY");return value && value[0]=='1';}();
|
||||
if(state.timings.size()>=4096u)return;
|
||||
if(scene_only && (!pass->second.depth || fb->second.info.width<1000 || fb->second.info.height<1000))return;
|
||||
const auto visit=++state.timing_visits[begin->framebuffer];
|
||||
// Warm the shaders/attachments first; retain only four measurements per
|
||||
// framebuffer. Query results are polled without WAIT and never stall frames.
|
||||
const unsigned cycle=visit%300;
|
||||
if(cycle<=6 || cycle>10)return;
|
||||
FovPassTiming timing;timing.command=command;timing.device=device;timing.framebuffer=begin->framebuffer;timing.width=fb->second.info.width;timing.height=fb->second.info.height;timing.samples=pass->second.samples;timing.depth=pass->second.depth;timing.foveated=foveated;
|
||||
timing.visit=visit;
|
||||
static const auto scope=ResolveProcessFunction<bool(*)(void**,int*,void**)>("mocktail_vr_current_eye");
|
||||
if(scope)(void)scope(&timing.owner,&timing.eye,&timing.guest_framebuffer);
|
||||
timing.color_format=pass->second.color_format;
|
||||
VkQueryPoolCreateInfo info{};info.sType=VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;info.queryType=VK_QUERY_TYPE_TIMESTAMP;info.queryCount=2;
|
||||
if(FovProc<PFN_vkCreateQueryPool>(device,"vkCreateQueryPool")(device,&info,nullptr,&timing.pool)!=VK_SUCCESS)return;
|
||||
if(dev->second.statistics && inline_draws && pass->second.subpasses==1) {
|
||||
info.queryType=VK_QUERY_TYPE_PIPELINE_STATISTICS;info.queryCount=1;
|
||||
info.pipelineStatistics=VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT;
|
||||
if(FovProc<PFN_vkCreateQueryPool>(device,"vkCreateQueryPool")(device,&info,nullptr,&timing.statistics_pool)==VK_SUCCESS) {
|
||||
FovProc<PFN_vkCmdResetQueryPool>(device,"vkCmdResetQueryPool")(command,timing.statistics_pool,0,1);
|
||||
}
|
||||
}
|
||||
FovProc<PFN_vkCmdResetQueryPool>(device,"vkCmdResetQueryPool")(command,timing.pool,0,2);
|
||||
FovProc<PFN_vkCmdWriteTimestamp>(device,"vkCmdWriteTimestamp")(command,VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,timing.pool,0);
|
||||
state.active_timings[command]=state.timings.size();state.timings.push_back(timing);
|
||||
}
|
||||
void FovTraceEnd(VkCommandBuffer command,VkDevice device) {
|
||||
if(!FovTraceEnabled())return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto active=state.active_timings.find(command);if(active==state.active_timings.end())return;
|
||||
auto& timing=state.timings[active->second];
|
||||
FovProc<PFN_vkCmdWriteTimestamp>(device,"vkCmdWriteTimestamp")(command,VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,timing.pool,1);
|
||||
timing.ended=true;state.active_timings.erase(active);
|
||||
}
|
||||
// Place statistics inside the pass so driver load/store reconstruction is
|
||||
// not mistaken for the guest's fragment shader work. Timestamps still enclose
|
||||
// the entire pass, including that cost.
|
||||
void FovTraceDrawScope(VkCommandBuffer command,VkDevice device,bool begin) {
|
||||
if(!FovTraceEnabled())return;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto active=state.active_timings.find(command);if(active==state.active_timings.end())return;
|
||||
const auto pool=state.timings[active->second].statistics_pool;if(!pool)return;
|
||||
if(begin)FovProc<PFN_vkCmdBeginQuery>(device,"vkCmdBeginQuery")(command,pool,0,0);
|
||||
else FovProc<PFN_vkCmdEndQuery>(device,"vkCmdEndQuery")(command,pool,0);
|
||||
}
|
||||
void FovFreeNeutral(VkDevice device, FovImage& data) {
|
||||
if(data.view) FovProc<PFN_vkDestroyImageView>(device,"vkDestroyImageView")(device,data.view,nullptr);
|
||||
if(data.image) FovProc<PFN_vkDestroyImage>(device,"vkDestroyImage")(device,data.image,nullptr);
|
||||
if(data.memory) FovProc<PFN_vkFreeMemory>(device,"vkFreeMemory")(device,data.memory,nullptr);
|
||||
data={};
|
||||
}
|
||||
void FovFreeGaze(VkDevice device,FovDevice& data) {
|
||||
for(auto& image:data.eyes)FovFreeNeutral(device,image);
|
||||
if(data.mapped)FovProc<PFN_vkUnmapMemory>(device,"vkUnmapMemory")(device,data.staging_memory);
|
||||
if(data.staging)FovProc<PFN_vkDestroyBuffer>(device,"vkDestroyBuffer")(device,data.staging,nullptr);
|
||||
if(data.staging_memory)FovProc<PFN_vkFreeMemory>(device,"vkFreeMemory")(device,data.staging_memory,nullptr);
|
||||
if(data.fence)FovProc<PFN_vkDestroyFence>(device,"vkDestroyFence")(device,data.fence,nullptr);
|
||||
if(data.pool)FovProc<PFN_vkDestroyCommandPool>(device,"vkDestroyCommandPool")(device,data.pool,nullptr);
|
||||
}
|
||||
bool FovInitializeGaze(VkDevice device,const VkPhysicalDeviceMemoryProperties& memory,unsigned family,FovDevice& data) {
|
||||
static const auto extent=ResolveProcessFunction<VkExtent2D(*)(VkDevice)>("mocktail_vr_xr_foveation_extent");
|
||||
const auto size=extent?extent(device):VkExtent2D{};
|
||||
if(!size.width || !size.height || size.width>1024 || size.height>1024)return false;
|
||||
data.width=size.width;data.height=size.height;
|
||||
static const auto host_enabled=ResolveProcessFunction<bool(*)(VkDevice)>("mocktail_vr_xr_foveation_host_copy_enabled");
|
||||
data.host_copy=host_enabled && host_enabled(device) &&
|
||||
FovProc<PFN_vkTransitionImageLayoutEXT>(device,"vkTransitionImageLayoutEXT") &&
|
||||
FovProc<PFN_vkCopyMemoryToImageEXT>(device,"vkCopyMemoryToImageEXT");
|
||||
const auto allocate=[&](VkMemoryRequirements req,VkMemoryPropertyFlags flags,VkDeviceMemory* result) {
|
||||
unsigned type=0;while(type<memory.memoryTypeCount && (!(req.memoryTypeBits&(1u<<type)) || (memory.memoryTypes[type].propertyFlags&flags)!=flags))++type;
|
||||
if(type==memory.memoryTypeCount)return false;
|
||||
VkMemoryAllocateInfo info{};info.sType=VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;info.allocationSize=req.size;info.memoryTypeIndex=type;
|
||||
return FovProc<PFN_vkAllocateMemory>(device,"vkAllocateMemory")(device,&info,nullptr,result)==VK_SUCCESS;
|
||||
};
|
||||
for(auto& eye:data.eyes) {
|
||||
VkImageCreateInfo image{};image.sType=VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;image.imageType=VK_IMAGE_TYPE_2D;image.format=VK_FORMAT_R16G16_SFLOAT;image.extent={size.width,size.height,1};image.mipLevels=image.arrayLayers=1;image.samples=VK_SAMPLE_COUNT_1_BIT;image.tiling=VK_IMAGE_TILING_OPTIMAL;image.usage=VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT|VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
if(data.host_copy)image.usage|=VK_IMAGE_USAGE_HOST_TRANSFER_BIT_EXT;
|
||||
auto created=FovProc<PFN_vkCreateImage>(device,"vkCreateImage")(device,&image,nullptr,&eye.image);
|
||||
if(created!=VK_SUCCESS && data.host_copy) {
|
||||
data.host_copy=false;image.usage&=~VK_IMAGE_USAGE_HOST_TRANSFER_BIT_EXT;
|
||||
created=FovProc<PFN_vkCreateImage>(device,"vkCreateImage")(device,&image,nullptr,&eye.image);
|
||||
}
|
||||
if(created!=VK_SUCCESS)return false;
|
||||
VkMemoryRequirements req{};FovProc<PFN_vkGetImageMemoryRequirements>(device,"vkGetImageMemoryRequirements")(device,eye.image,&req);
|
||||
if(!allocate(req,0,&eye.memory) || FovProc<PFN_vkBindImageMemory>(device,"vkBindImageMemory")(device,eye.image,eye.memory,0)!=VK_SUCCESS)return false;
|
||||
VkImageViewCreateInfo view{};view.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;view.image=eye.image;view.viewType=VK_IMAGE_VIEW_TYPE_2D;view.format=image.format;view.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
|
||||
if(FovProc<PFN_vkCreateImageView>(device,"vkCreateImageView")(device,&view,nullptr,&eye.view)!=VK_SUCCESS)return false;
|
||||
}
|
||||
VkBufferCreateInfo buffer{};buffer.sType=VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;buffer.size=VkDeviceSize(size.width)*size.height*4;buffer.usage=VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||
if(FovProc<PFN_vkCreateBuffer>(device,"vkCreateBuffer")(device,&buffer,nullptr,&data.staging)!=VK_SUCCESS)return false;
|
||||
VkMemoryRequirements req{};FovProc<PFN_vkGetBufferMemoryRequirements>(device,"vkGetBufferMemoryRequirements")(device,data.staging,&req);
|
||||
if(!allocate(req,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,&data.staging_memory) || FovProc<PFN_vkBindBufferMemory>(device,"vkBindBufferMemory")(device,data.staging,data.staging_memory,0)!=VK_SUCCESS || FovProc<PFN_vkMapMemory>(device,"vkMapMemory")(device,data.staging_memory,0,VK_WHOLE_SIZE,0,&data.mapped)!=VK_SUCCESS)return false;
|
||||
VkCommandPoolCreateInfo cp{};cp.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;cp.queueFamilyIndex=family;cp.flags=VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||
if(FovProc<PFN_vkCreateCommandPool>(device,"vkCreateCommandPool")(device,&cp,nullptr,&data.pool)!=VK_SUCCESS)return false;
|
||||
VkCommandBufferAllocateInfo ca{};ca.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;ca.commandPool=data.pool;ca.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;ca.commandBufferCount=1;
|
||||
if(FovProc<PFN_vkAllocateCommandBuffers>(device,"vkAllocateCommandBuffers")(device,&ca,&data.command)!=VK_SUCCESS)return false;
|
||||
VkFenceCreateInfo fc{};fc.sType=VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
if(FovProc<PFN_vkCreateFence>(device,"vkCreateFence")(device,&fc,nullptr,&data.fence)!=VK_SUCCESS)return false;
|
||||
FovProc<PFN_vkGetDeviceQueue>(device,"vkGetDeviceQueue")(device,family,0,&data.queue);
|
||||
return data.queue!=VK_NULL_HANDLE;
|
||||
}
|
||||
VkExtent2D FovGazeRegionBound(const FovDevice& data,const FovRenderPass& pass,
|
||||
const VkFramebufferCreateInfo& framebuffer) {
|
||||
if(framebuffer.pNext || framebuffer.flags || framebuffer.layers!=1)return data.max_density_region;
|
||||
auto driver=data.tile_driver;driver.driver_name=data.driver_name;driver.driver_info=data.driver_info;
|
||||
auto identity=pass.tile_identity;
|
||||
identity.width=framebuffer.width;identity.height=framebuffer.height;
|
||||
identity.framebuffer_layers=framebuffer.layers;
|
||||
identity.density_offsets_enabled=data.density_offsets_enabled;
|
||||
const auto known=mocktail::vr::KnownFoveationTileBound(driver,identity);
|
||||
return known?VkExtent2D{known->width,known->height}:data.max_density_region;
|
||||
}
|
||||
VkImageView FovUploadGaze(VkDevice device,FovDevice& data,const FovRenderPass& pass,
|
||||
VkExtent2D region_bound,int eye,std::uint64_t frame,float x,float y,int level,float radius,unsigned eye_width,unsigned eye_height) {
|
||||
if(!data.gaze_ready || eye<0 || eye>1 || !eye_width || !eye_height || !std::isfinite(x) || !std::isfinite(y))return VK_NULL_HANDLE;
|
||||
const auto layout=mocktail::vr::internal::TurnipDensityLayout(data.width,data.height,
|
||||
eye_width,eye_height,data.min_density_region.width,data.min_density_region.height,
|
||||
data.max_density_region.width,data.max_density_region.height);
|
||||
if(data.framebuffer_density_mapping && !layout.valid)return VK_NULL_HANDLE;
|
||||
const auto center=data.framebuffer_density_mapping?
|
||||
mocktail::vr::internal::QuantizeFramebufferGaze(layout,x,y):
|
||||
mocktail::vr::internal::QuantizeGazeCenter(data.width,data.height,x,y);
|
||||
FovDevice::FocusKey key{center.x,center.y,eye_width,eye_height,data.width,data.height,
|
||||
data.framebuffer_density_mapping?layout.texel_width:0,
|
||||
data.framebuffer_density_mapping?layout.texel_height:0,
|
||||
level,mocktail::vr::internal::ClampGazeRadius(radius)};
|
||||
key.guard_width=region_bound.width;key.guard_height=region_bound.height;
|
||||
key.color_format=unsigned(pass.color_format);key.depth_format=unsigned(pass.depth_format);
|
||||
key.samples=pass.samples;key.attachments=pass.attachments;
|
||||
const auto remember=[&] {data.frames[eye]=frame;data.focus_keys[eye]=key;data.focus_cached[eye]=true;};
|
||||
const auto cache=mocktail::vr::internal::DensityCacheAction(data.initialized[eye],
|
||||
data.focus_cached[eye],data.frames[eye],frame,data.focus_keys[eye],key);
|
||||
if(cache==mocktail::vr::internal::GazeDensityCacheAction::kNeutral)return VK_NULL_HANDLE;
|
||||
if(cache==mocktail::vr::internal::GazeDensityCacheAction::kReuse) {
|
||||
remember();return data.eyes[eye].view;
|
||||
}
|
||||
const float guard_x=.5f*std::min(region_bound.width,eye_width)/eye_width;
|
||||
const float guard_y=.5f*std::min(region_bound.height,eye_height)/eye_height;
|
||||
auto& pixels=data.scratch_pixels[eye];
|
||||
if(data.framebuffer_density_mapping) {
|
||||
if(!mocktail::vr::internal::FillFramebufferGazeDensityMap(pixels,layout,x,y,level,radius,
|
||||
guard_x*eye_width,guard_y*eye_height))return VK_NULL_HANDLE;
|
||||
} else mocktail::vr::internal::FillGazeDensityMap(pixels,data.width,data.height,x,y,level,radius,guard_x,guard_y);
|
||||
if(data.initialized[eye] && data.uploaded[eye]==pixels) {
|
||||
remember();return data.eyes[eye].view;
|
||||
}
|
||||
auto& densities=data.scratch_densities[eye];
|
||||
mocktail::vr::internal::FillHalfDensityMap(densities,pixels);
|
||||
if(data.host_copy) {
|
||||
// The previous frame's eye-copy fence completes all preceding graphics
|
||||
// reads before these owned maps are reused. Non-dynamic maps can be read
|
||||
// by the driver at BeginRenderPass, so finish the CPU copy here. Queue
|
||||
// submission supplies host-write visibility without an upload submission.
|
||||
VkResult status=VK_SUCCESS;
|
||||
if(!data.initialized[eye]) {
|
||||
VkHostImageLayoutTransitionInfoEXT transition{};transition.sType=VK_STRUCTURE_TYPE_HOST_IMAGE_LAYOUT_TRANSITION_INFO_EXT;
|
||||
transition.image=data.eyes[eye].image;transition.oldLayout=VK_IMAGE_LAYOUT_UNDEFINED;transition.newLayout=VK_IMAGE_LAYOUT_GENERAL;transition.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
|
||||
status=FovProc<PFN_vkTransitionImageLayoutEXT>(device,"vkTransitionImageLayoutEXT")(device,1,&transition);
|
||||
if(status==VK_SUCCESS)data.initialized[eye]=true;
|
||||
}
|
||||
if(status==VK_SUCCESS) {
|
||||
VkMemoryToImageCopyEXT region{};region.sType=VK_STRUCTURE_TYPE_MEMORY_TO_IMAGE_COPY_EXT;region.pHostPointer=densities.data();region.imageSubresource={VK_IMAGE_ASPECT_COLOR_BIT,0,0,1};region.imageExtent={data.width,data.height,1};
|
||||
VkCopyMemoryToImageInfoEXT copy{};copy.sType=VK_STRUCTURE_TYPE_COPY_MEMORY_TO_IMAGE_INFO_EXT;copy.dstImage=data.eyes[eye].image;copy.dstImageLayout=VK_IMAGE_LAYOUT_GENERAL;copy.regionCount=1;copy.pRegions=®ion;
|
||||
status=FovProc<PFN_vkCopyMemoryToImageEXT>(device,"vkCopyMemoryToImageEXT")(device,©);
|
||||
}
|
||||
if(status==VK_SUCCESS) {
|
||||
data.uploaded[eye]=pixels;remember();
|
||||
static unsigned reports=0;if(reports++<2)std::fprintf(stderr," [vr-foveation] gaze map uploaded on CPU without GPU queue/fence wait\n");
|
||||
return data.eyes[eye].view;
|
||||
}
|
||||
data.host_copy=false;
|
||||
std::fprintf(stderr," [vr-foveation] host map copy failed (%d); using synchronized staging fallback\n",int(status));
|
||||
}
|
||||
std::memcpy(data.mapped,densities.data(),densities.size()*sizeof(std::uint16_t));
|
||||
if(FovProc<PFN_vkResetCommandPool>(device,"vkResetCommandPool")(device,data.pool,0)!=VK_SUCCESS)return VK_NULL_HANDLE;
|
||||
VkCommandBufferBeginInfo begin{};begin.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;begin.flags=VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if(FovProc<PFN_vkBeginCommandBuffer>(device,"vkBeginCommandBuffer")(data.command,&begin)!=VK_SUCCESS)return VK_NULL_HANDLE;
|
||||
VkImageMemoryBarrier barrier{};barrier.sType=VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;barrier.image=data.eyes[eye].image;barrier.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};barrier.srcQueueFamilyIndex=barrier.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED;barrier.oldLayout=data.initialized[eye]?VK_IMAGE_LAYOUT_GENERAL:VK_IMAGE_LAYOUT_UNDEFINED;barrier.newLayout=VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;barrier.srcAccessMask=data.initialized[eye]?VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT:0;barrier.dstAccessMask=VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
auto pipeline_barrier=FovProc<PFN_vkCmdPipelineBarrier>(device,"vkCmdPipelineBarrier");
|
||||
pipeline_barrier(data.command,data.initialized[eye]?VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT:VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,0,0,nullptr,0,nullptr,1,&barrier);
|
||||
VkBufferImageCopy copy{};copy.imageSubresource={VK_IMAGE_ASPECT_COLOR_BIT,0,0,1};copy.imageExtent={data.width,data.height,1};
|
||||
FovProc<PFN_vkCmdCopyBufferToImage>(device,"vkCmdCopyBufferToImage")(data.command,data.staging,barrier.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,1,©);
|
||||
barrier.oldLayout=VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;barrier.newLayout=VK_IMAGE_LAYOUT_GENERAL;barrier.srcAccessMask=VK_ACCESS_TRANSFER_WRITE_BIT;barrier.dstAccessMask=VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT|VK_ACCESS_HOST_READ_BIT;
|
||||
pipeline_barrier(data.command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT|VK_PIPELINE_STAGE_HOST_BIT,0,0,nullptr,0,nullptr,1,&barrier);
|
||||
if(FovProc<PFN_vkEndCommandBuffer>(device,"vkEndCommandBuffer")(data.command)!=VK_SUCCESS)return VK_NULL_HANDLE;
|
||||
if(FovProc<PFN_vkResetFences>(device,"vkResetFences")(device,1,&data.fence)!=VK_SUCCESS)return VK_NULL_HANDLE;
|
||||
VkSubmitInfo submit{};submit.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;submit.commandBufferCount=1;submit.pCommandBuffers=&data.command;
|
||||
if(mocktail_vulkan_submit_synchronized(FovProc<PFN_vkQueueSubmit>(device,"vkQueueSubmit"),device,data.queue,1,&submit,data.fence)!=VK_SUCCESS)return VK_NULL_HANDLE;
|
||||
// Non-dynamic density maps may be read on the CPU at CmdBeginRenderPass.
|
||||
// A submitted copy alone is insufficient: finish it before returning a view.
|
||||
if(FovProc<PFN_vkWaitForFences>(device,"vkWaitForFences")(device,1,&data.fence,VK_TRUE,5000000000ull)!=VK_SUCCESS) {
|
||||
(void)mocktail_vulkan_idle_synchronized(FovProc<PFN_vkDeviceWaitIdle>(device,"vkDeviceWaitIdle"),device);data.gaze_ready=false;return VK_NULL_HANDLE;
|
||||
}
|
||||
data.initialized[eye]=true;data.uploaded[eye]=pixels;remember();
|
||||
return data.eyes[eye].view;
|
||||
}
|
||||
void FovRegisterDevice(VkDevice device, VkPhysicalDevice physical,
|
||||
const VkPhysicalDeviceMemoryProperties& memory, std::uint32_t family, bool statistics,
|
||||
mocktail::graphics::internal::FoveationDevicePolicy device_policy) {
|
||||
static const auto enabled=ResolveProcessFunction<bool(*)(VkDevice)>("mocktail_vr_xr_foveation_enabled");
|
||||
if(!enabled || !enabled(device) || family==UINT32_MAX) return;
|
||||
auto& state=FovState(); std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if(state.devices.count(device)) return;
|
||||
FovDevice data;data.statistics=statistics;
|
||||
VkCommandPool pool{}; VkFence fence{}; bool submitted=false;
|
||||
const auto cleanup=[&]{
|
||||
if(submitted) (void)mocktail_vulkan_idle_synchronized(FovProc<PFN_vkDeviceWaitIdle>(device,"vkDeviceWaitIdle"),device);
|
||||
if(fence) FovProc<PFN_vkDestroyFence>(device,"vkDestroyFence")(device,fence,nullptr);
|
||||
if(pool) FovProc<PFN_vkDestroyCommandPool>(device,"vkDestroyCommandPool")(device,pool,nullptr);
|
||||
};
|
||||
VkPhysicalDeviceFragmentDensityMapPropertiesEXT density{};
|
||||
density.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_PROPERTIES_EXT;
|
||||
VkPhysicalDeviceDriverProperties driver{};
|
||||
driver.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES;density.pNext=&driver;
|
||||
VkPhysicalDeviceIDProperties id{};
|
||||
id.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES;driver.pNext=&id;
|
||||
VkPhysicalDeviceProperties2 properties{};
|
||||
properties.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties.pNext=&density;
|
||||
const auto get_properties=HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2","vkGetPhysicalDeviceProperties2KHR");
|
||||
if(!get_properties) return;
|
||||
get_properties(physical,&properties);
|
||||
const auto format_properties=HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties",nullptr);
|
||||
VkFormatProperties format{};
|
||||
if(!format_properties)return;
|
||||
format_properties(physical,VK_FORMAT_R16G16_SFLOAT,&format);
|
||||
if(!(format.optimalTilingFeatures&VK_FORMAT_FEATURE_FRAGMENT_DENSITY_MAP_BIT_EXT))return;
|
||||
data.min_density_region=density.minFragmentDensityTexelSize;
|
||||
data.max_density_region=density.maxFragmentDensityTexelSize;
|
||||
data.framebuffer_density_mapping=driver.driverID==VK_DRIVER_ID_MESA_TURNIP;
|
||||
data.tile_driver.vendor_id=properties.properties.vendorID;
|
||||
data.tile_driver.device_id=properties.properties.deviceID;
|
||||
data.tile_driver.driver_version=properties.properties.driverVersion;
|
||||
data.tile_driver.driver_id=unsigned(driver.driverID);
|
||||
std::copy_n(id.driverUUID,data.tile_driver.driver_uuid.size(),data.tile_driver.driver_uuid.begin());
|
||||
data.driver_name.assign(driver.driverName,strnlen(driver.driverName,sizeof(driver.driverName)));
|
||||
data.driver_info.assign(driver.driverInfo,strnlen(driver.driverInfo,sizeof(driver.driverInfo)));
|
||||
// Offsets are not part of the calibrated density sampling contract. Merely
|
||||
// enabling either spelling of that extension keeps the conservative bound.
|
||||
data.density_offsets_enabled=device_policy.density_offsets_enabled;
|
||||
data.timestamp_period=properties.properties.limits.timestampPeriod;
|
||||
const auto get_families=HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties",nullptr);
|
||||
if(get_families) {
|
||||
unsigned count=0;get_families(physical,&count,nullptr);
|
||||
if(count<=256 && family<count){std::vector<VkQueueFamilyProperties> families(count);get_families(physical,&count,families.data());data.timestamp_bits=families[family].timestampValidBits;}
|
||||
}
|
||||
if(!density.maxFragmentDensityTexelSize.width || !density.maxFragmentDensityTexelSize.height) return;
|
||||
// One full-density map covers every legal guest framebuffer. Its contents
|
||||
// are finished on the GPU before any CmdBeginRenderPass host read can occur.
|
||||
VkImageCreateInfo image{}; image.sType=VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
image.imageType=VK_IMAGE_TYPE_2D; image.format=VK_FORMAT_R16G16_SFLOAT;
|
||||
image.extent={(properties.properties.limits.maxFramebufferWidth+density.maxFragmentDensityTexelSize.width-1)/density.maxFragmentDensityTexelSize.width,
|
||||
(properties.properties.limits.maxFramebufferHeight+density.maxFragmentDensityTexelSize.height-1)/density.maxFragmentDensityTexelSize.height,1};
|
||||
image.mipLevels=1;image.arrayLayers=1;image.samples=VK_SAMPLE_COUNT_1_BIT;image.tiling=VK_IMAGE_TILING_OPTIMAL;
|
||||
image.usage=VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT|VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
bool ok=FovProc<PFN_vkCreateImage>(device,"vkCreateImage")(device,&image,nullptr,&data.image)==VK_SUCCESS;
|
||||
if(ok) {
|
||||
VkMemoryRequirements req{};FovProc<PFN_vkGetImageMemoryRequirements>(device,"vkGetImageMemoryRequirements")(device,data.image,&req);
|
||||
std::uint32_t type=0;while(type<memory.memoryTypeCount && !(req.memoryTypeBits&(1u<<type))) ++type;
|
||||
VkMemoryAllocateInfo alloc{};alloc.sType=VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;alloc.allocationSize=req.size;alloc.memoryTypeIndex=type;
|
||||
ok=type<memory.memoryTypeCount && FovProc<PFN_vkAllocateMemory>(device,"vkAllocateMemory")(device,&alloc,nullptr,&data.memory)==VK_SUCCESS;
|
||||
if(ok)ok=FovProc<PFN_vkBindImageMemory>(device,"vkBindImageMemory")(device,data.image,data.memory,0)==VK_SUCCESS;
|
||||
}
|
||||
if(ok) {
|
||||
VkImageViewCreateInfo view{};view.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;view.image=data.image;view.viewType=VK_IMAGE_VIEW_TYPE_2D;view.format=image.format;view.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
|
||||
ok=FovProc<PFN_vkCreateImageView>(device,"vkCreateImageView")(device,&view,nullptr,&data.view)==VK_SUCCESS;
|
||||
}
|
||||
VkCommandBuffer command{};
|
||||
if(ok) {
|
||||
VkCommandPoolCreateInfo cp{};cp.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;cp.queueFamilyIndex=family;cp.flags=VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||
ok=FovProc<PFN_vkCreateCommandPool>(device,"vkCreateCommandPool")(device,&cp,nullptr,&pool)==VK_SUCCESS;
|
||||
VkCommandBufferAllocateInfo ca{};ca.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;ca.commandPool=pool;ca.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;ca.commandBufferCount=1;
|
||||
if(ok)ok=FovProc<PFN_vkAllocateCommandBuffers>(device,"vkAllocateCommandBuffers")(device,&ca,&command)==VK_SUCCESS;
|
||||
}
|
||||
if(ok) {
|
||||
VkCommandBufferBeginInfo begin{};begin.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;begin.flags=VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
ok=FovProc<PFN_vkBeginCommandBuffer>(device,"vkBeginCommandBuffer")(command,&begin)==VK_SUCCESS;
|
||||
if(ok) {
|
||||
VkImageMemoryBarrier barrier{};barrier.sType=VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;barrier.image=data.image;barrier.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};barrier.srcQueueFamilyIndex=barrier.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED;barrier.oldLayout=VK_IMAGE_LAYOUT_UNDEFINED;barrier.newLayout=VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;barrier.dstAccessMask=VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
const auto pipeline_barrier=FovProc<PFN_vkCmdPipelineBarrier>(device,"vkCmdPipelineBarrier");
|
||||
pipeline_barrier(command,VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,0,0,nullptr,0,nullptr,1,&barrier);
|
||||
const VkClearColorValue full{{1.f,1.f,1.f,1.f}};
|
||||
FovProc<PFN_vkCmdClearColorImage>(device,"vkCmdClearColorImage")(command,data.image,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,&full,1,&barrier.subresourceRange);
|
||||
barrier.oldLayout=VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;barrier.newLayout=VK_IMAGE_LAYOUT_GENERAL;barrier.srcAccessMask=VK_ACCESS_TRANSFER_WRITE_BIT;barrier.dstAccessMask=VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT|VK_ACCESS_HOST_READ_BIT;
|
||||
pipeline_barrier(command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT|VK_PIPELINE_STAGE_HOST_BIT,0,0,nullptr,0,nullptr,1,&barrier);
|
||||
ok=FovProc<PFN_vkEndCommandBuffer>(device,"vkEndCommandBuffer")(command)==VK_SUCCESS;
|
||||
}
|
||||
}
|
||||
if(ok) {
|
||||
VkFenceCreateInfo fc{};fc.sType=VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||
ok=FovProc<PFN_vkCreateFence>(device,"vkCreateFence")(device,&fc,nullptr,&fence)==VK_SUCCESS;
|
||||
if(ok) {
|
||||
VkQueue queue{};FovProc<PFN_vkGetDeviceQueue>(device,"vkGetDeviceQueue")(device,family,0,&queue);
|
||||
VkSubmitInfo submit{};submit.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;submit.commandBufferCount=1;submit.pCommandBuffers=&command;
|
||||
ok=mocktail_vulkan_submit_synchronized(FovProc<PFN_vkQueueSubmit>(device,"vkQueueSubmit"),device,queue,1,&submit,fence)==VK_SUCCESS;
|
||||
submitted=ok;
|
||||
if(ok)ok=FovProc<PFN_vkWaitForFences>(device,"vkWaitForFences")(device,1,&fence,VK_TRUE,5000000000ull)==VK_SUCCESS;
|
||||
}
|
||||
}
|
||||
cleanup();
|
||||
if(ok) {data.gaze_ready=FovInitializeGaze(device,memory,family,data);state.devices.emplace(device,data);std::fprintf(stderr," [vr-foveation] full-detail fallback map initialized %ux%u; protected gaze maps %s\n",image.extent.width,image.extent.height,data.gaze_ready?"ready":"unavailable");}
|
||||
else {FovFreeNeutral(device,data);std::fprintf(stderr," [vr-foveation] fallback map failed; render-pass foveation disabled\n");}
|
||||
}
|
||||
void FovDestroyDevice(VkDevice device) {
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if(!state.devices.count(device)) return;
|
||||
(void)mocktail_vulkan_idle_synchronized(FovProc<PFN_vkDeviceWaitIdle>(device,"vkDeviceWaitIdle"),device);
|
||||
for(auto& timing:state.timings)if(timing.device==device && timing.pool) {FovProc<PFN_vkDestroyQueryPool>(device,"vkDestroyQueryPool")(device,timing.pool,nullptr);timing.pool=VK_NULL_HANDLE;timing.reported=true;}
|
||||
for(auto& timing:state.timings)if(timing.device==device && timing.statistics_pool) {FovProc<PFN_vkDestroyQueryPool>(device,"vkDestroyQueryPool")(device,timing.statistics_pool,nullptr);timing.statistics_pool=VK_NULL_HANDLE;}
|
||||
for(auto it=state.framebuffers.begin();it!=state.framebuffers.end();) {
|
||||
if(it->second.device!=device) {++it;continue;}
|
||||
for(const auto& clone:it->second.clones)FovProc<PFN_vkDestroyFramebuffer>(device,"vkDestroyFramebuffer")(device,clone.framebuffer,nullptr);
|
||||
it=state.framebuffers.erase(it);
|
||||
}
|
||||
for(auto it=state.passes.begin();it!=state.passes.end();)if(it->second.device==device)it=state.passes.erase(it);else++it;
|
||||
FovFreeGaze(device,state.devices.at(device));FovFreeNeutral(device,state.devices.at(device));state.devices.erase(device);
|
||||
}
|
||||
bool FovColorFormat(VkFormat format) {
|
||||
return format==VK_FORMAT_R8G8B8A8_UNORM || format==VK_FORMAT_R8G8B8A8_SRGB ||
|
||||
format==VK_FORMAT_B8G8R8A8_UNORM || format==VK_FORMAT_B8G8R8A8_SRGB ||
|
||||
format==VK_FORMAT_R16G16B16A16_SFLOAT || format==VK_FORMAT_B10G11R11_UFLOAT_PACK32 ||
|
||||
format==VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
}
|
||||
VkResult FovCreateRenderPass(VkDevice device,const VkRenderPassCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkRenderPass* result) {
|
||||
const auto host=HostDispatchForDevice(device).create_render_pass;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if(!info || !state.devices.count(device) || info->pNext || !info->attachmentCount)
|
||||
return host(device,info,allocator,result);
|
||||
FovRenderPass pass{device,info->attachmentCount};pass.subpasses=info->subpassCount;
|
||||
for(unsigned i=0;i<info->attachmentCount;++i)
|
||||
pass.samples=std::max(pass.samples,unsigned(info->pAttachments[i].samples));
|
||||
if(info->subpassCount==1) {
|
||||
const auto& subpass=info->pSubpasses[0];
|
||||
pass.colors=subpass.colorAttachmentCount;
|
||||
pass.depth=subpass.pDepthStencilAttachment && subpass.pDepthStencilAttachment->attachment!=VK_ATTACHMENT_UNUSED;
|
||||
if(pass.depth && subpass.pDepthStencilAttachment->attachment<info->attachmentCount)
|
||||
pass.depth_format=info->pAttachments[subpass.pDepthStencilAttachment->attachment].format;
|
||||
if(pass.colors==1 && subpass.pColorAttachments[0].attachment<info->attachmentCount)
|
||||
pass.color_format=info->pAttachments[subpass.pColorAttachments[0].attachment].format;
|
||||
}
|
||||
// A neutral density map can still force tiled rendering and reconstruction.
|
||||
// Only attach one when the pass can actually receive an eye gaze map. Other
|
||||
// passes retain the driver's normal render-mode choice and native stores.
|
||||
const bool eligible=mocktail::vr::IsFoveationScenePassCompatible(*info) &&
|
||||
FovColorFormat(pass.color_format);
|
||||
if(!eligible) {
|
||||
const auto status=host(device,info,allocator,result);
|
||||
if(status==VK_SUCCESS && FovTraceEnabled())state.passes[*result]=pass;
|
||||
return status;
|
||||
}
|
||||
const auto& subpass=info->pSubpasses[0];
|
||||
const unsigned color_index=subpass.pColorAttachments[0].attachment;
|
||||
const unsigned depth_index=subpass.pDepthStencilAttachment->attachment;
|
||||
bool strict_contract=info->flags==0 && info->attachmentCount==2 &&
|
||||
subpass.flags==0 && subpass.pipelineBindPoint==VK_PIPELINE_BIND_POINT_GRAPHICS &&
|
||||
subpass.inputAttachmentCount==0 && subpass.pResolveAttachments==nullptr &&
|
||||
subpass.preserveAttachmentCount==0 && color_index<2 && depth_index<2 && color_index!=depth_index;
|
||||
for(unsigned i=0;i<info->attachmentCount;++i)strict_contract&=info->pAttachments[i].flags==0;
|
||||
if(strict_contract) {
|
||||
// This legacy render-pass path rejects pNext above, excluding multiview,
|
||||
// MSRTSS, custom resolves, density offsets and shading-rate attachments.
|
||||
auto& identity=pass.tile_identity;
|
||||
identity.color_format=unsigned(pass.color_format);identity.depth_format=unsigned(pass.depth_format);
|
||||
identity.color_samples=unsigned(info->pAttachments[color_index].samples);
|
||||
identity.depth_samples=unsigned(info->pAttachments[depth_index].samples);
|
||||
identity.subpass_count=1;identity.total_attachment_count=3;
|
||||
identity.color_attachment_count=1;identity.gmem_attachment_count=2;
|
||||
identity.has_depth_attachment=true;identity.has_density_attachment=true;
|
||||
}
|
||||
std::vector<VkAttachmentDescription> attachments(info->pAttachments,info->pAttachments+info->attachmentCount);
|
||||
VkAttachmentDescription map{};map.format=VK_FORMAT_R16G16_SFLOAT;map.samples=VK_SAMPLE_COUNT_1_BIT;map.loadOp=VK_ATTACHMENT_LOAD_OP_LOAD;map.storeOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;map.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE;map.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;map.initialLayout=map.finalLayout=VK_IMAGE_LAYOUT_GENERAL;
|
||||
attachments.push_back(map);
|
||||
VkRenderPassFragmentDensityMapCreateInfoEXT density{};density.sType=VK_STRUCTURE_TYPE_RENDER_PASS_FRAGMENT_DENSITY_MAP_CREATE_INFO_EXT;density.fragmentDensityMapAttachment={info->attachmentCount,VK_IMAGE_LAYOUT_GENERAL};
|
||||
auto adjusted=*info;adjusted.pNext=&density;adjusted.attachmentCount=attachments.size();adjusted.pAttachments=attachments.data();
|
||||
const auto status=host(device,&adjusted,allocator,result);
|
||||
if(status==VK_SUCCESS) {pass.attached=true;state.passes[*result]=pass;}
|
||||
return status;
|
||||
}
|
||||
VkResult FovCreateFramebuffer(VkDevice device,const VkFramebufferCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkFramebuffer* result) {
|
||||
const auto host=HostDispatchForDevice(device).create_framebuffer;
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if(!info || !state.passes.count(info->renderPass))return host(device,info,allocator,result);
|
||||
if(!state.passes.at(info->renderPass).attached) {
|
||||
const auto status=host(device,info,allocator,result);
|
||||
if(status==VK_SUCCESS && !info->pNext && !info->flags && info->layers==1) {
|
||||
std::vector<VkImageView> views(info->pAttachments,info->pAttachments+info->attachmentCount);
|
||||
state.framebuffers.emplace(*result,FovFramebuffer{device,*info,std::move(views),{}});
|
||||
}
|
||||
return status;
|
||||
}
|
||||
if(info->pNext || info->flags || info->layers!=1 || info->attachmentCount!=state.passes.at(info->renderPass).attachments) {
|
||||
std::fprintf(stderr," [vr-foveation] unsupported framebuffer contract; refusing altered render pass\n");return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
}
|
||||
std::vector<VkImageView> views(info->pAttachments,info->pAttachments+info->attachmentCount);
|
||||
views.push_back(state.devices.at(device).view);
|
||||
auto adjusted=*info;adjusted.attachmentCount=views.size();adjusted.pAttachments=views.data();
|
||||
const auto status=host(device,&adjusted,allocator,result);
|
||||
if(status==VK_SUCCESS)state.framebuffers.emplace(*result,FovFramebuffer{device,*info,std::move(views),{}});
|
||||
return status;
|
||||
}
|
||||
void FovForgetFramebuffer(VkDevice device,VkFramebuffer framebuffer) {
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
auto it=state.framebuffers.find(framebuffer);if(it==state.framebuffers.end())return;
|
||||
for(const auto& clone:it->second.clones)HostDispatchForDevice(device).destroy_framebuffer(device,clone.framebuffer,nullptr);
|
||||
state.framebuffers.erase(it);
|
||||
state.timing_visits.erase(framebuffer);
|
||||
}
|
||||
void FovForgetRenderPass(VkRenderPass pass) {
|
||||
auto& state=FovState();std::lock_guard<std::mutex> lock(state.mutex);state.passes.erase(pass);
|
||||
// A destroyed render pass may legally leave its framebuffer alive. Retire
|
||||
// its clone cache key, retaining the framebuffer until its owner's teardown.
|
||||
for(auto& item:state.framebuffers)for(auto& clone:item.second.clones)if(clone.pass==pass)clone.pass=VK_NULL_HANDLE;
|
||||
}
|
||||
VkFramebuffer FovFramebufferForBegin(VkDevice device,const VkRenderPassBeginInfo* begin) {
|
||||
static const auto select=ResolveProcessFunction<bool(*)(VkDevice,VkFramebuffer,std::uint32_t,std::uint32_t,float*,float*,float*,std::uint64_t*,int*,int*)>("mocktail_vr_xr_foveation_render_focus");
|
||||
if(!select || !begin || begin->pNext)return begin?begin->framebuffer:VK_NULL_HANDLE;
|
||||
const bool scene_enabled=FovSceneEnabled();
|
||||
auto& state=FovState();
|
||||
std::uint32_t width=0,height=0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto fb=state.framebuffers.find(begin->framebuffer);
|
||||
const auto pass=state.passes.find(begin->renderPass);
|
||||
if(fb==state.framebuffers.end() || pass==state.passes.end() ||
|
||||
fb->second.device!=device || pass->second.device!=device || !pass->second.attached || !state.devices.count(device))return begin->framebuffer;
|
||||
const auto& info=fb->second.info;const auto& rp=pass->second;
|
||||
const bool color=FovColorFormat(rp.color_format);
|
||||
// Full eye-sized native 1x/2x/4x/8x 3D passes only. Smaller effects, partial
|
||||
// render areas, depth-only shadows and unknown/MRT contracts stay neutral.
|
||||
if(scene_enabled && rp.subpasses==1 && rp.colors==1 && color && rp.depth &&
|
||||
mocktail::vr::IsFoveationSceneSampleCount(static_cast<VkSampleCountFlagBits>(rp.samples)) &&
|
||||
info.layers==1 && begin->renderArea.offset.x==0 && begin->renderArea.offset.y==0 &&
|
||||
begin->renderArea.extent.width==info.width && begin->renderArea.extent.height==info.height) {
|
||||
width=info.width;height=info.height;
|
||||
}
|
||||
}
|
||||
// Do not hold the adapter lock when calling the backend: fence completion
|
||||
// reports can acquire these locks in the opposite direction.
|
||||
float x=0,y=0,radius=.22f;std::uint64_t frame=0;int eye=0,level=1;
|
||||
if(!select(device,begin->framebuffer,width,height,&x,&y,&radius,&frame,&eye,&level))return begin->framebuffer;
|
||||
static const bool alternate=[] {const char* value=std::getenv("MOCKTAIL_VR_FOVEATION_ALTERNATE");return value && value[0]=='1';}();
|
||||
if(FovTraceEnabled() && alternate && (frame&1u))return begin->framebuffer;
|
||||
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
auto it=state.framebuffers.find(begin->framebuffer);
|
||||
if(it==state.framebuffers.end() || !state.passes.count(begin->renderPass))return begin->framebuffer;
|
||||
if(!state.devices.count(device))return begin->framebuffer;
|
||||
const auto& selected_pass=state.passes.at(begin->renderPass);
|
||||
const auto region_bound=FovGazeRegionBound(state.devices.at(device),selected_pass,it->second.info);
|
||||
const auto view=FovUploadGaze(device,state.devices.at(device),selected_pass,region_bound,
|
||||
eye,frame,x,y,level,radius,it->second.info.width,it->second.info.height);if(!view)return begin->framebuffer;
|
||||
auto& fb=it->second;
|
||||
for(const auto& clone:fb.clones)if(clone.view==view && clone.pass==begin->renderPass)return clone.framebuffer;
|
||||
// The backend verifies final eye handles, or the current eye getter and
|
||||
// renderer generation for an exact eye-sized scene intermediate.
|
||||
std::vector<VkImageView> attachments=fb.attachments;attachments.back()=view;
|
||||
auto adjusted=fb.info;adjusted.renderPass=begin->renderPass;adjusted.attachmentCount=attachments.size();adjusted.pAttachments=attachments.data();
|
||||
VkFramebuffer clone{};
|
||||
if(HostDispatchForDevice(device).create_framebuffer(device,&adjusted,nullptr,&clone)!=VK_SUCCESS)return begin->framebuffer;
|
||||
fb.clones.push_back({view,begin->renderPass,clone});
|
||||
if(selected_pass.samples>1)++state.msaa_selected;
|
||||
if(++state.selected<=8 || (selected_pass.samples>1 && state.msaa_selected<=8)) {
|
||||
const auto& data=state.devices.at(device);
|
||||
const auto& pass=state.passes.at(begin->renderPass);
|
||||
const float guard_x=.5f*std::min(region_bound.width,fb.info.width)/fb.info.width;
|
||||
const float guard_y=.5f*std::min(region_bound.height,fb.info.height)/fb.info.height;
|
||||
const bool calibrated=region_bound.width!=data.max_density_region.width || region_bound.height!=data.max_density_region.height;
|
||||
std::fprintf(stderr," [vr-foveation] verified %s render pass uses gaze map with full-density focus disk; eye=%d samples=%u color_resolve=%d extent=%ux%u color_format=%d depth_format=%d density_extent=%ux%u density_mapping=%s density_max_region=%ux%u selected_tile_bound=%ux%u calibrated=%d guard_uv=%.6f,%.6f guard_pixels=%.1f,%.1f gaze_ndc=%.4f,%.4f focus_radius=%.3f profile=%d\n",
|
||||
width?"3D scene":"eye output",eye,pass.samples,int(pass.attachments==3),fb.info.width,fb.info.height,
|
||||
int(pass.color_format),int(pass.depth_format),data.width,data.height,
|
||||
data.framebuffer_density_mapping?"framebuffer":"normalized",
|
||||
data.max_density_region.width,data.max_density_region.height,
|
||||
region_bound.width,region_bound.height,int(calibrated),
|
||||
guard_x,guard_y,guard_x*fb.info.width,guard_y*fb.info.height,x,y,radius,level);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
#if !defined(MOCKTAIL_ENABLE_FSR3)
|
||||
// Native builds preserve guest allocations/stores and perform no temporal
|
||||
// resource, shader, pipeline or command tracking.
|
||||
[[maybe_unused]] bool Fsr3InputsAvailable() { return false; }
|
||||
[[maybe_unused]] bool Fsr3InputsEnabled() { return false; }
|
||||
[[maybe_unused]] bool Fsr3ShaderDumpEnabled() { return false; }
|
||||
inline VkResult Fsr3CreateImage(VkDevice device,const VkImageCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkImage* image,VkImageCreateInfo* effective) {
|
||||
if(info && effective)*effective=*info;
|
||||
return State().etc2.CreateImage(device,info,allocator,image);
|
||||
}
|
||||
inline VkResult Fsr3CreateRenderPass(VkDevice device,const VkRenderPassCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkRenderPass* pass) {
|
||||
return FovCreateRenderPass(device,info,allocator,pass);
|
||||
}
|
||||
inline VkRenderPass Fsr3RenderPassForBegin(VkDevice,VkRenderPass pass) { return pass; }
|
||||
inline void Fsr3RecordView(VkImageView,const VkImageViewCreateInfo*) {}
|
||||
inline void Fsr3RecordFramebuffer(VkDevice,VkFramebuffer,const VkFramebufferCreateInfo*) {}
|
||||
inline void Fsr3SceneBegin(VkDevice,VkCommandBuffer,const VkRenderPassBeginInfo*,VkSubpassContents) {}
|
||||
inline void Fsr3SceneEnd(VkDevice,VkCommandBuffer) {}
|
||||
inline void Fsr3NotePipeline(VkDevice,VkCommandBuffer,VkPipelineBindPoint,VkPipeline) {}
|
||||
inline void Fsr3NoteDraw(VkCommandBuffer) {}
|
||||
inline void Fsr3ForgetPipeline(VkPipeline) {}
|
||||
inline void Fsr3ForgetShader(VkShaderModule) {}
|
||||
inline void Fsr3ForgetImage(VkImage) {}
|
||||
inline void Fsr3ForgetView(VkImageView) {}
|
||||
inline void Fsr3ForgetFramebuffer(VkFramebuffer) {}
|
||||
inline void Fsr3ForgetPass(VkRenderPass) {}
|
||||
inline void Fsr3ForgetDevice(VkDevice) {}
|
||||
inline void Fsr3ResetCommand(VkCommandBuffer) {}
|
||||
inline void Fsr3DumpShader(VkDevice,VkShaderModule,const VkShaderModuleCreateInfo*) {}
|
||||
inline void Fsr3RecordDiagnosticPass(VkRenderPass,const VkRenderPassCreateInfo*) {}
|
||||
inline void Fsr3DumpGraphicsPipelines(VkDevice,unsigned,const VkGraphicsPipelineCreateInfo*,const VkPipeline*) {}
|
||||
#else
|
||||
// Temporal scene resources are captured on the guest command that produced
|
||||
// them. Nothing in this adapter assumes an image remains valid after return.
|
||||
struct Fsr3ImageRecord {
|
||||
VkDevice device{};
|
||||
VkImageCreateInfo info{};
|
||||
bool transferable = false;
|
||||
};
|
||||
struct Fsr3ViewRecord { VkImageViewCreateInfo info{}; };
|
||||
struct Fsr3ShaderRecord { VkDevice device{}; std::uint64_t hash=0; };
|
||||
struct Fsr3PipelineRecord { VkDevice device{}; int kind=0; };
|
||||
struct Fsr3PassRecord {
|
||||
VkDevice device{};
|
||||
unsigned color = VK_ATTACHMENT_UNUSED, depth = VK_ATTACHMENT_UNUSED;
|
||||
VkImageLayout color_layout{}, depth_layout{};
|
||||
VkAttachmentLoadOp depth_load_op = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
VkRenderPass capture_pass = VK_NULL_HANDLE;
|
||||
bool owns_capture_pass = false;
|
||||
bool has_allocator = false;
|
||||
VkAllocationCallbacks allocator{};
|
||||
};
|
||||
struct Fsr3FramebufferRecord {
|
||||
VkDevice device{};
|
||||
VkRenderPass pass{};
|
||||
unsigned width = 0, height = 0;
|
||||
std::vector<VkImageView> views;
|
||||
};
|
||||
struct Fsr3ActiveScene {
|
||||
mocktail::vr::TemporalSceneInputs inputs;
|
||||
bool motion_bound=false,other_bound=false,unsupported_motion=false;
|
||||
};
|
||||
struct Fsr3InputsState {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<VkImage,Fsr3ImageRecord> images;
|
||||
std::unordered_map<VkImageView,Fsr3ViewRecord> views;
|
||||
std::unordered_map<VkRenderPass,Fsr3PassRecord> passes;
|
||||
std::unordered_map<VkFramebuffer,Fsr3FramebufferRecord> framebuffers;
|
||||
std::unordered_map<VkCommandBuffer,Fsr3ActiveScene> active;
|
||||
std::unordered_map<VkPipeline,Fsr3PipelineRecord> pipeline_motion;
|
||||
std::unordered_map<VkCommandBuffer,VkDevice> command_devices;
|
||||
std::unordered_map<VkCommandBuffer,int> bound_graphics;
|
||||
std::unordered_map<VkRenderPass,std::vector<VkFormat>> diagnostic_formats;
|
||||
std::unordered_map<VkShaderModule,Fsr3ShaderRecord> diagnostic_shaders;
|
||||
};
|
||||
Fsr3InputsState& Fsr3State() { static Fsr3InputsState state; return state; }
|
||||
|
||||
bool Fsr3InputsAvailable() {
|
||||
static const auto available=ResolveProcessFunction<bool(*)()>(
|
||||
"mocktail_vr_xr_fsr3_inputs_available");
|
||||
return available && available();
|
||||
}
|
||||
|
||||
bool Fsr3InputsEnabled() {
|
||||
static const auto enabled=ResolveProcessFunction<bool(*)()>(
|
||||
"mocktail_vr_xr_fsr3_inputs_enabled");
|
||||
return enabled && enabled();
|
||||
}
|
||||
|
||||
VkResult Fsr3CreateImage(VkDevice device,const VkImageCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkImage* image,
|
||||
VkImageCreateInfo* effective) {
|
||||
if(!info)return State().etc2.CreateImage(device,info,allocator,image);
|
||||
*effective=*info;
|
||||
const bool candidate=Fsr3InputsAvailable() &&
|
||||
mocktail::vr::IsTemporalImageCandidate(*info);
|
||||
if(candidate) {
|
||||
effective->usage|=VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
auto status=State().etc2.CreateImage(device,effective,allocator,image);
|
||||
// Optional preservation must never make a previously supported render
|
||||
// target fail creation. The callback will reject an uncapturable target.
|
||||
if(status!=VK_SUCCESS && candidate && effective->usage!=info->usage) {
|
||||
*effective=*info;
|
||||
status=State().etc2.CreateImage(device,info,allocator,image);
|
||||
}
|
||||
if(status==VK_SUCCESS && image && Fsr3InputsAvailable()) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
auto saved=*effective;saved.pNext=nullptr;saved.pQueueFamilyIndices=nullptr;
|
||||
state.images[*image]={device,saved,candidate &&
|
||||
bool(effective->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)};
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void Fsr3RecordView(VkImageView view,const VkImageViewCreateInfo* info) {
|
||||
if(!info || !Fsr3InputsAvailable())return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
auto saved=*info;saved.pNext=nullptr;
|
||||
state.views[view]={saved};
|
||||
}
|
||||
|
||||
VkResult Fsr3CreateRenderPass(VkDevice device,const VkRenderPassCreateInfo* info,
|
||||
const VkAllocationCallbacks* allocator,VkRenderPass* pass) {
|
||||
if(!Fsr3InputsAvailable() || !info || info->pNext || info->flags ||
|
||||
info->subpassCount!=1 || !info->pSubpasses || !info->pAttachments)
|
||||
return FovCreateRenderPass(device,info,allocator,pass);
|
||||
const auto& subpass=info->pSubpasses[0];
|
||||
const auto depth=subpass.pDepthStencilAttachment;
|
||||
if(subpass.pipelineBindPoint!=VK_PIPELINE_BIND_POINT_GRAPHICS ||
|
||||
subpass.colorAttachmentCount!=1 || !subpass.pColorAttachments ||
|
||||
subpass.inputAttachmentCount || subpass.pResolveAttachments)
|
||||
return FovCreateRenderPass(device,info,allocator,pass);
|
||||
const unsigned color_index=subpass.pColorAttachments[0].attachment;
|
||||
const unsigned depth_index=depth ? depth->attachment : VK_ATTACHMENT_UNUSED;
|
||||
const bool has_depth=depth_index<info->attachmentCount;
|
||||
if(color_index>=info->attachmentCount || color_index==depth_index ||
|
||||
(!mocktail::vr::IsTemporalColorFormat(info->pAttachments[color_index].format) &&
|
||||
!mocktail::vr::IsTemporalMotionFormat(info->pAttachments[color_index].format)) ||
|
||||
(has_depth && !mocktail::vr::IsTemporalDepthFormat(info->pAttachments[depth_index].format)) ||
|
||||
info->pAttachments[color_index].samples!=VK_SAMPLE_COUNT_1_BIT ||
|
||||
(has_depth && info->pAttachments[depth_index].samples!=VK_SAMPLE_COUNT_1_BIT))
|
||||
return FovCreateRenderPass(device,info,allocator,pass);
|
||||
// Keep Native's original stores. Load/store operations do not participate in
|
||||
// render-pass compatibility, so its framebuffer and pipelines also support
|
||||
// a preserving variant selected only when temporal capture is active.
|
||||
const auto status=FovCreateRenderPass(device,info,allocator,pass);
|
||||
if(status!=VK_SUCCESS || !pass)return status;
|
||||
std::vector<VkAttachmentDescription> attachments(
|
||||
info->pAttachments,info->pAttachments+info->attachmentCount);
|
||||
if(has_depth)attachments[depth_index].storeOp=VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachments[color_index].storeOp=VK_ATTACHMENT_STORE_OP_STORE;
|
||||
auto adjusted=*info;adjusted.pAttachments=attachments.data();
|
||||
Fsr3PassRecord record;
|
||||
record.device=device;record.color=color_index;record.depth=depth_index;
|
||||
record.color_layout=attachments[color_index].finalLayout;
|
||||
record.depth_layout=has_depth?attachments[depth_index].finalLayout:VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
record.depth_load_op=has_depth?attachments[depth_index].loadOp:VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
record.has_allocator=allocator!=nullptr;if(allocator)record.allocator=*allocator;
|
||||
const bool already_preserved=info->pAttachments[color_index].storeOp==VK_ATTACHMENT_STORE_OP_STORE &&
|
||||
(!has_depth || info->pAttachments[depth_index].storeOp==VK_ATTACHMENT_STORE_OP_STORE);
|
||||
if(already_preserved)record.capture_pass=*pass;
|
||||
else {
|
||||
VkRenderPass preserving{};
|
||||
if(FovCreateRenderPass(device,&adjusted,allocator,&preserving)==VK_SUCCESS) {
|
||||
record.capture_pass=preserving;record.owns_capture_pass=true;
|
||||
} else {
|
||||
std::fprintf(stderr," [vr-fsr3] preserving render-pass variant unavailable; retaining native pass\n");
|
||||
}
|
||||
}
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.passes[*pass]=record;
|
||||
return status;
|
||||
}
|
||||
|
||||
VkRenderPass Fsr3RenderPassForBegin(VkDevice device,VkRenderPass pass) {
|
||||
if(!Fsr3InputsEnabled())return pass;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto found=state.passes.find(pass);
|
||||
return found!=state.passes.end() && found->second.device==device &&
|
||||
found->second.capture_pass?found->second.capture_pass:pass;
|
||||
}
|
||||
|
||||
void Fsr3RecordFramebuffer(VkDevice device,VkFramebuffer framebuffer,
|
||||
const VkFramebufferCreateInfo* info) {
|
||||
if(!Fsr3InputsAvailable() || !info || info->pNext || info->flags ||
|
||||
info->layers!=1 || !info->pAttachments)return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.framebuffers[framebuffer]={device,info->renderPass,info->width,info->height,
|
||||
{info->pAttachments,info->pAttachments+info->attachmentCount}};
|
||||
}
|
||||
|
||||
bool Fsr3Source(VkDevice device,VkImageView view,unsigned width,unsigned height,
|
||||
bool depth,VkImage* image,VkFormat* format,VkImageSubresourceRange* range) {
|
||||
// Caller owns Fsr3State().mutex.
|
||||
const auto& state=Fsr3State();const auto found=state.views.find(view);
|
||||
if(found==state.views.end())return false;
|
||||
const auto& info=found->second.info;const auto source=state.images.find(info.image);
|
||||
if(source==state.images.end() || source->second.device!=device ||
|
||||
!source->second.transferable || info.viewType!=VK_IMAGE_VIEW_TYPE_2D ||
|
||||
info.flags || info.subresourceRange.baseMipLevel ||
|
||||
info.subresourceRange.baseArrayLayer ||
|
||||
(info.subresourceRange.levelCount!=1 &&
|
||||
info.subresourceRange.levelCount!=VK_REMAINING_MIP_LEVELS) ||
|
||||
(info.subresourceRange.layerCount!=1 &&
|
||||
info.subresourceRange.layerCount!=VK_REMAINING_ARRAY_LAYERS) ||
|
||||
source->second.info.extent.width!=width ||
|
||||
source->second.info.extent.height!=height)return false;
|
||||
const auto aspect=depth ? mocktail::vr::TemporalDepthAspect(info.format)
|
||||
: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
if(!aspect || !(info.subresourceRange.aspectMask & aspect) ||
|
||||
(depth ? !mocktail::vr::IsTemporalDepthFormat(info.format)
|
||||
: (!mocktail::vr::IsTemporalColorFormat(info.format) &&
|
||||
!mocktail::vr::IsTemporalMotionFormat(info.format))))return false;
|
||||
*image=info.image;*format=info.format;
|
||||
*range={aspect,0,1,0,1};return true;
|
||||
}
|
||||
|
||||
void Fsr3SceneBegin(VkDevice device,VkCommandBuffer command,
|
||||
const VkRenderPassBeginInfo* begin,VkSubpassContents contents) {
|
||||
if(!Fsr3InputsEnabled())return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.active.erase(command);state.command_devices[command]=device;
|
||||
if(!begin || begin->pNext || contents!=VK_SUBPASS_CONTENTS_INLINE ||
|
||||
begin->renderArea.offset.x || begin->renderArea.offset.y)return;
|
||||
const auto fb=state.framebuffers.find(begin->framebuffer);
|
||||
const auto pass=state.passes.find(begin->renderPass);
|
||||
if(fb==state.framebuffers.end() || pass==state.passes.end() ||
|
||||
!pass->second.capture_pass ||
|
||||
fb->second.device!=device || pass->second.device!=device ||
|
||||
fb->second.pass!=begin->renderPass ||
|
||||
fb->second.width!=begin->renderArea.extent.width ||
|
||||
fb->second.height!=begin->renderArea.extent.height ||
|
||||
fb->second.width<256 || fb->second.height<256 ||
|
||||
pass->second.color>=fb->second.views.size() ||
|
||||
(pass->second.depth!=VK_ATTACHMENT_UNUSED &&
|
||||
pass->second.depth>=fb->second.views.size()) ||
|
||||
pass->second.color_layout==VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
(pass->second.depth!=VK_ATTACHMENT_UNUSED &&
|
||||
pass->second.depth_layout==VK_IMAGE_LAYOUT_UNDEFINED))return;
|
||||
mocktail::vr::TemporalSceneInputs inputs;
|
||||
static const auto current=ResolveProcessFunction<bool(*)(void**,int*,void**)>(
|
||||
"mocktail_vr_current_eye");
|
||||
if(!current || !current(&inputs.owner,&inputs.eye,&inputs.guest_framebuffer) ||
|
||||
!inputs.owner || !inputs.guest_framebuffer || inputs.eye<0 || inputs.eye>1)return;
|
||||
inputs.framebuffer=begin->framebuffer;
|
||||
inputs.width=fb->second.width;inputs.height=fb->second.height;
|
||||
inputs.color_layout=pass->second.color_layout;
|
||||
inputs.depth_layout=pass->second.depth_layout;
|
||||
if(pass->second.depth_load_op==VK_ATTACHMENT_LOAD_OP_CLEAR &&
|
||||
begin->pClearValues && pass->second.depth<begin->clearValueCount) {
|
||||
inputs.depth_clear=begin->pClearValues[pass->second.depth].depthStencil.depth;
|
||||
inputs.depth_clear_valid=std::isfinite(inputs.depth_clear);
|
||||
}
|
||||
if(!Fsr3Source(device,fb->second.views[pass->second.color],inputs.width,
|
||||
inputs.height,false,&inputs.color,&inputs.color_format,&inputs.color_range))return;
|
||||
if(pass->second.depth!=VK_ATTACHMENT_UNUSED &&
|
||||
!Fsr3Source(device,fb->second.views[pass->second.depth],inputs.width,
|
||||
inputs.height,true,&inputs.depth,&inputs.depth_format,&inputs.depth_range))return;
|
||||
state.active[command]={inputs};
|
||||
|
||||
}
|
||||
|
||||
void Fsr3NotePipeline(VkDevice device,VkCommandBuffer command,VkPipelineBindPoint point,
|
||||
VkPipeline pipeline) {
|
||||
if(!Fsr3InputsEnabled() || point!=VK_PIPELINE_BIND_POINT_GRAPHICS)return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto found=state.pipeline_motion.find(pipeline);
|
||||
const int kind=found==state.pipeline_motion.end()?0:found->second.kind;
|
||||
state.bound_graphics[command]=kind;state.command_devices[command]=device;
|
||||
}
|
||||
|
||||
void Fsr3NoteDraw(VkCommandBuffer command) {
|
||||
if(!Fsr3InputsEnabled())return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto active=state.active.find(command);if(active==state.active.end())return;
|
||||
const auto bound=state.bound_graphics.find(command);
|
||||
const int kind=bound==state.bound_graphics.end()?0:bound->second;
|
||||
if(kind==1)active->second.motion_bound=true;
|
||||
else if(kind==2)active->second.unsupported_motion=true;
|
||||
else active->second.other_bound=true;
|
||||
}
|
||||
|
||||
void Fsr3ForgetPipeline(VkPipeline pipeline) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.pipeline_motion.erase(pipeline);
|
||||
}
|
||||
|
||||
void Fsr3SceneEnd(VkDevice device,VkCommandBuffer command) {
|
||||
if(!Fsr3InputsEnabled())return;
|
||||
Fsr3ActiveScene active;
|
||||
{
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto found=state.active.find(command);if(found==state.active.end())return;
|
||||
active=found->second;state.active.erase(found);
|
||||
}
|
||||
const auto& inputs=active.inputs;
|
||||
if(active.unsupported_motion)return;
|
||||
if(active.motion_bound) {
|
||||
if(active.other_bound || !mocktail::vr::IsTemporalMotionFormat(inputs.color_format))return;
|
||||
mocktail::vr::TemporalMotionInputs motion;
|
||||
motion.framebuffer=inputs.framebuffer;motion.image=inputs.color;
|
||||
motion.format=inputs.color_format;motion.layout=inputs.color_layout;
|
||||
motion.range=inputs.color_range;motion.width=inputs.width;motion.height=inputs.height;
|
||||
motion.owner=inputs.owner;motion.guest_framebuffer=inputs.guest_framebuffer;motion.eye=inputs.eye;
|
||||
static const auto capture=ResolveProcessFunction<void(*)(VkDevice,VkCommandBuffer,
|
||||
const mocktail::vr::TemporalMotionInputs*)>("mocktail_vr_xr_fsr3_motion_end");
|
||||
if(capture)capture(device,command,&motion);
|
||||
return;
|
||||
}
|
||||
if(!inputs.depth || !mocktail::vr::IsTemporalColorFormat(inputs.color_format))return;
|
||||
// Never hold the adapter mutex while entering the backend (its submission
|
||||
// completion paths can notify the adapter in the opposite direction).
|
||||
static const auto capture=ResolveProcessFunction<void(*)(VkDevice,
|
||||
VkCommandBuffer,const mocktail::vr::TemporalSceneInputs*)>(
|
||||
"mocktail_vr_xr_fsr3_scene_end");
|
||||
if(capture)capture(device,command,&inputs);
|
||||
}
|
||||
|
||||
void Fsr3ForgetImage(VkImage image) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.images.erase(image);
|
||||
for(auto it=state.active.begin();it!=state.active.end();)
|
||||
if(it->second.inputs.color==image || it->second.inputs.depth==image)it=state.active.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
void Fsr3ForgetView(VkImageView view) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);state.views.erase(view);
|
||||
}
|
||||
void Fsr3ForgetFramebuffer(VkFramebuffer framebuffer) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.framebuffers.erase(framebuffer);
|
||||
for(auto it=state.active.begin();it!=state.active.end();)
|
||||
if(it->second.inputs.framebuffer==framebuffer)it=state.active.erase(it);else ++it;
|
||||
}
|
||||
void Fsr3DestroyPassVariant(const Fsr3PassRecord& record) {
|
||||
if(!record.owns_capture_pass || !record.capture_pass)return;
|
||||
FovForgetRenderPass(record.capture_pass);
|
||||
const auto destroy=HostDispatchForDevice(record.device).destroy_render_pass;
|
||||
if(destroy)destroy(record.device,record.capture_pass,
|
||||
record.has_allocator?&record.allocator:nullptr);
|
||||
}
|
||||
void Fsr3ForgetPass(VkRenderPass pass) {
|
||||
Fsr3PassRecord record;
|
||||
{
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
const auto found=state.passes.find(pass);
|
||||
if(found!=state.passes.end()){record=found->second;state.passes.erase(found);}
|
||||
state.diagnostic_formats.erase(pass);
|
||||
}
|
||||
Fsr3DestroyPassVariant(record);
|
||||
}
|
||||
void Fsr3ResetCommand(VkCommandBuffer command) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.active.erase(command);state.bound_graphics.erase(command);state.command_devices.erase(command);
|
||||
}
|
||||
void Fsr3ForgetDevice(VkDevice device) {
|
||||
std::vector<Fsr3PassRecord> variants;
|
||||
auto& state=Fsr3State();std::unique_lock<std::mutex> lock(state.mutex);
|
||||
const auto belongs=[&](VkImage image) {
|
||||
const auto found=state.images.find(image);
|
||||
return found!=state.images.end() && found->second.device==device;
|
||||
};
|
||||
for(auto it=state.active.begin();it!=state.active.end();)
|
||||
if(belongs(it->second.inputs.color) || belongs(it->second.inputs.depth))it=state.active.erase(it);else ++it;
|
||||
for(auto it=state.views.begin();it!=state.views.end();)
|
||||
if(belongs(it->second.info.image))it=state.views.erase(it);else ++it;
|
||||
for(auto it=state.images.begin();it!=state.images.end();)
|
||||
if(it->second.device==device)it=state.images.erase(it);else ++it;
|
||||
for(auto it=state.passes.begin();it!=state.passes.end();)
|
||||
if(it->second.device==device) {
|
||||
variants.push_back(it->second);state.diagnostic_formats.erase(it->first);
|
||||
it=state.passes.erase(it);
|
||||
} else ++it;
|
||||
for(auto it=state.framebuffers.begin();it!=state.framebuffers.end();)
|
||||
if(it->second.device==device)it=state.framebuffers.erase(it);else ++it;
|
||||
for(auto it=state.command_devices.begin();it!=state.command_devices.end();)
|
||||
if(it->second==device) {
|
||||
state.active.erase(it->first);state.bound_graphics.erase(it->first);
|
||||
it=state.command_devices.erase(it);
|
||||
} else ++it;
|
||||
for(auto it=state.pipeline_motion.begin();it!=state.pipeline_motion.end();)
|
||||
if(it->second.device==device)it=state.pipeline_motion.erase(it);else ++it;
|
||||
for(auto it=state.diagnostic_shaders.begin();it!=state.diagnostic_shaders.end();)
|
||||
if(it->second.device==device)it=state.diagnostic_shaders.erase(it);else ++it;
|
||||
lock.unlock();
|
||||
for(const auto& record:variants)Fsr3DestroyPassVariant(record);
|
||||
}
|
||||
|
||||
void Fsr3ForgetShader(VkShaderModule shader) {
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.diagnostic_shaders.erase(shader);
|
||||
}
|
||||
|
||||
bool Fsr3ShaderDumpEnabled() {
|
||||
static const bool enabled=[] {
|
||||
const auto* directory=std::getenv("MOCKTAIL_VR_FSR3_SHADER_DUMP");
|
||||
return directory && directory[0]=='/';
|
||||
}();return enabled;
|
||||
}
|
||||
void Fsr3DumpShader(VkDevice device,VkShaderModule module,const VkShaderModuleCreateInfo* info) {
|
||||
if((!Fsr3ShaderDumpEnabled() && !Fsr3InputsAvailable()) || !info || !info->pCode ||
|
||||
info->codeSize<20 || info->codeSize%4 || info->pCode[0]!=0x07230203u ||
|
||||
info->codeSize>16*1024*1024)return;
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
const auto* bytes=reinterpret_cast<const unsigned char*>(info->pCode);
|
||||
for(std::size_t i=0;i<info->codeSize;++i){hash^=bytes[i];hash*=1099511628211ull;}
|
||||
{
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.diagnostic_shaders[module]={device,hash};
|
||||
}
|
||||
if(!Fsr3ShaderDumpEnabled())return;
|
||||
static std::mutex mutex;static std::unordered_map<std::uint64_t,bool> dumped;
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if(dumped.count(hash) || dumped.size()>=4096)return;
|
||||
char name[64];std::snprintf(name,sizeof(name),"/%016llx.spv",
|
||||
static_cast<unsigned long long>(hash));
|
||||
const std::string path=std::string(std::getenv("MOCKTAIL_VR_FSR3_SHADER_DUMP"))+name;
|
||||
if(auto* file=std::fopen(path.c_str(),"wb")) {
|
||||
const bool written=std::fwrite(bytes,1,info->codeSize,file)==info->codeSize;
|
||||
std::fclose(file);if(written)dumped[hash]=true;
|
||||
}
|
||||
}
|
||||
|
||||
void Fsr3RecordDiagnosticPass(VkRenderPass pass,const VkRenderPassCreateInfo* info) {
|
||||
if(!Fsr3ShaderDumpEnabled() || !info || !info->pAttachments)return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
std::vector<VkFormat> formats;
|
||||
for(unsigned i=0;i<info->attachmentCount;++i)formats.push_back(info->pAttachments[i].format);
|
||||
state.diagnostic_formats[pass]=std::move(formats);
|
||||
}
|
||||
|
||||
void Fsr3DumpGraphicsPipelines(VkDevice device,unsigned count,const VkGraphicsPipelineCreateInfo* infos,
|
||||
const VkPipeline* pipelines) {
|
||||
if((!Fsr3ShaderDumpEnabled() && !Fsr3InputsAvailable()) || !infos)return;
|
||||
auto& state=Fsr3State();std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if(pipelines)for(unsigned index=0;index<count;++index) {
|
||||
std::uint64_t vertex=0,fragment=0;
|
||||
for(unsigned i=0;i<infos[index].stageCount;++i) {
|
||||
const auto& stage=infos[index].pStages[i];
|
||||
const auto hash=state.diagnostic_shaders.find(stage.module);
|
||||
if(hash==state.diagnostic_shaders.end())continue;
|
||||
if(stage.stage==VK_SHADER_STAGE_VERTEX_BIT)vertex=hash->second.hash;
|
||||
if(stage.stage==VK_SHADER_STAGE_FRAGMENT_BIT)fragment=hash->second.hash;
|
||||
}
|
||||
const int kind=mocktail::vr::IsTemporalClipMotionShaders(vertex,fragment)?1:
|
||||
fragment==0x821fff499ec5bdbbull?2:0;
|
||||
state.pipeline_motion[pipelines[index]]={device,kind};
|
||||
}
|
||||
if(!Fsr3ShaderDumpEnabled())return;
|
||||
const std::string path=std::string(std::getenv("MOCKTAIL_VR_FSR3_SHADER_DUMP"))+"/pipelines.jsonl";
|
||||
auto* file=std::fopen(path.c_str(),"a");if(!file)return;
|
||||
for(unsigned index=0;index<count;++index) {
|
||||
const auto& info=infos[index];
|
||||
std::fprintf(file,"{\"pass\":\"%p\",\"subpass\":%u,\"formats\":[",
|
||||
reinterpret_cast<void*>(info.renderPass),info.subpass);
|
||||
const auto formats=state.diagnostic_formats.find(info.renderPass);
|
||||
if(formats!=state.diagnostic_formats.end())
|
||||
for(unsigned i=0;i<formats->second.size();++i)
|
||||
std::fprintf(file,"%s%d",i?",":"",int(formats->second[i]));
|
||||
std::fprintf(file,"],\"shaders\":[");
|
||||
for(unsigned i=0;i<info.stageCount;++i) {
|
||||
const auto hash=state.diagnostic_shaders.find(info.pStages[i].module);
|
||||
std::fprintf(file,"%s{\"stage\":%u,\"hash\":\"%016llx\"}",i?",":"",
|
||||
unsigned(info.pStages[i].stage),static_cast<unsigned long long>(
|
||||
hash==state.diagnostic_shaders.end()?0:hash->second.hash));
|
||||
}
|
||||
std::fprintf(file,"]}\n");
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
#endif
|
||||
+15
-1
@@ -11,6 +11,7 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cerrno>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
@@ -63,6 +64,7 @@ void* mocktail_gameactivity_on_trim_memory_native = nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
float NativeVrRefreshRate();
|
||||
thread_local JNIEnv* g_thread_local_env = nullptr;
|
||||
thread_local JNIEnv g_thread_env_storage = {};
|
||||
thread_local VM* g_thread_vm_instance = nullptr;
|
||||
@@ -318,7 +320,8 @@ jint IntResultForName(jmethodID method_id) {
|
||||
return 33;
|
||||
}
|
||||
if (method == JniMethod::kGetRefreshRate) {
|
||||
return 60;
|
||||
const float hz=NativeVrRefreshRate();
|
||||
return hz>0.f?static_cast<jint>(hz+.5f):60;
|
||||
}
|
||||
if (method == JniMethod::kGetSdkVersion) {
|
||||
return 33;
|
||||
@@ -341,6 +344,14 @@ jint IntResultForName(jmethodID method_id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float NativeVrRefreshRate() {
|
||||
const char* enabled=std::getenv("MOCKTAIL_VR_ENABLED");
|
||||
const char* text=std::getenv("MOCKTAIL_VR_DISPLAY_HZ");
|
||||
if(!enabled || std::strcmp(enabled,"1")!=0 || !text)return 0.f;
|
||||
char* end=nullptr;const float hz=std::strtof(text,&end);
|
||||
return end && *end=='\0' && std::isfinite(hz) && hz>=30.f && hz<=240.f?hz:0.f;
|
||||
}
|
||||
|
||||
jint IntResultForMethod(jmethodID method_id) {
|
||||
return IntResultForName(method_id);
|
||||
}
|
||||
@@ -4227,6 +4238,9 @@ jfloat FloatResultForReceiverMethod(jobject obj, jmethodID method_id) {
|
||||
if (!name) {
|
||||
return 0.0f;
|
||||
}
|
||||
if(std::strcmp(name,"getRefreshRate")==0) {
|
||||
const float hz=NativeVrRefreshRate();if(hz>0.f)return hz;
|
||||
}
|
||||
jfloat value = FloatFieldValue(obj, name);
|
||||
if (value != 0.0f) {
|
||||
return value;
|
||||
|
||||
@@ -309,6 +309,7 @@ struct EngineStartupContext {
|
||||
jnivm::RobloxAuthIdentity account_identity;
|
||||
const mocktail::runtime::SecureRobloxCredential* roblox_credential;
|
||||
mocktail::runtime::RobloxGameSessionRuntime* game_session_runtime;
|
||||
mocktail::runtime::LuauCodegenMenuSwitch* luau_codegen_switch;
|
||||
bool run_prepare_jni;
|
||||
bool run_set_asset_path;
|
||||
bool call_real_set_asset_path;
|
||||
@@ -3977,6 +3978,13 @@ void* EngineStartupThread(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
// All client-setting initialization is complete. Verify the native switch
|
||||
// before any LuaApp startup can compile menu scripts. Failure leaves the
|
||||
// client's own codegen policy unchanged.
|
||||
if (context->luau_codegen_switch != nullptr) {
|
||||
context->luau_codegen_switch->PrepareForMenus();
|
||||
}
|
||||
|
||||
if (context->run_set_init_params) {
|
||||
env = ensure_env();
|
||||
if (!context->run_native_settings && context->native_set_device_info &&
|
||||
@@ -4705,7 +4713,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
mocktail::runtime::kLuauCodegenAllFastFlagName);
|
||||
if (flag == nullptr) {
|
||||
std::cerr << " [luau-codegen] DebugLuauCodegenAll not found; "
|
||||
"leaving it on in games\n";
|
||||
"automatic menu codegen disabled\n";
|
||||
} else {
|
||||
std::cout << " [luau-codegen] menus-only switch at +0x" << std::hex
|
||||
<< (reinterpret_cast<std::uintptr_t>(flag) - base)
|
||||
@@ -6962,6 +6970,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
dependencies.account_identity(),
|
||||
&dependencies.roblox_credential(),
|
||||
game_session_runtime.get(),
|
||||
&luau_codegen_switch,
|
||||
run_prepare_jni,
|
||||
run_set_asset_path,
|
||||
call_real_set_asset_path,
|
||||
|
||||
+19
-4
@@ -905,6 +905,11 @@ int main(int argc, char* argv[]) {
|
||||
"(MOCKTAIL_VR_BACKEND=native).\n";
|
||||
}
|
||||
}
|
||||
#ifdef MOCKTAIL_ENABLE_FSR3
|
||||
constexpr bool preserve_temporal_depth = true;
|
||||
#else
|
||||
constexpr bool preserve_temporal_depth = false;
|
||||
#endif
|
||||
const std::filesystem::path fflags_path =
|
||||
paths.config_root() / "fflags.json";
|
||||
const auto fflags = mocktail::services::LoadAndMergeFflagsFile(
|
||||
@@ -946,7 +951,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
if (!mocktail::runtime::MergeVrClientSettingsOverrides(
|
||||
vr_active, audio_capture_overrides,
|
||||
&audio_capture_overrides, &command_line_error)) {
|
||||
&audio_capture_overrides, &command_line_error, preserve_temporal_depth)) {
|
||||
std::cerr << "[FATAL] Cannot apply VR policy: " << command_line_error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -967,7 +972,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
if (!mocktail::runtime::MergeVrClientSettingsOverrides(
|
||||
vr_active, fast_flags_overrides,
|
||||
&fast_flags_overrides, &command_line_error)) {
|
||||
&fast_flags_overrides, &command_line_error, preserve_temporal_depth)) {
|
||||
std::cerr << "[FATAL] Cannot apply VR policy: " << command_line_error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -1219,6 +1224,8 @@ int main(int argc, char* argv[]) {
|
||||
std::cout
|
||||
<< " [auth] explicit guest identity selected for production VM\n";
|
||||
}
|
||||
const bool user_disabled_luau_codegen_switch =
|
||||
environment.GetOr("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH", "") == "0";
|
||||
unsetenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH");
|
||||
if (runtime_config.config.desktop_playability()) {
|
||||
std::filesystem::path assets = paths.DefaultAssetPath();
|
||||
@@ -1247,9 +1254,16 @@ int main(int argc, char* argv[]) {
|
||||
const bool user_set_luau_codegen =
|
||||
mocktail::runtime::ClientSettingsSetLuauCodegenAll(
|
||||
user_client_settings);
|
||||
bool default_codegen_at_startup = !user_disabled_luau_codegen_switch;
|
||||
#if defined(__aarch64__)
|
||||
// The ARM client must prove a safe menus-only flag before enabling it.
|
||||
default_codegen_at_startup =
|
||||
default_codegen_at_startup && !runtime_config.config.vr_enabled();
|
||||
#endif
|
||||
if (!mocktail::runtime::MergeDesktopAppPolicyClientSettingsOverride(
|
||||
desktop_policy.policy_json, user_client_settings,
|
||||
&desktop_client_settings, &command_line_error)) {
|
||||
&desktop_client_settings, &command_line_error,
|
||||
default_codegen_at_startup)) {
|
||||
std::cerr << "[FATAL] Cannot activate desktop Roblox app-policy: "
|
||||
<< command_line_error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
@@ -1260,7 +1274,8 @@ int main(int argc, char* argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
setenv("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH",
|
||||
user_set_luau_codegen ? "0" : "1", 1);
|
||||
(user_set_luau_codegen || user_disabled_luau_codegen_switch)
|
||||
? "0" : "1", 1);
|
||||
std::cout << " [runtime] desktop app-policy ready: normalized="
|
||||
<< desktop_policy.normalized_policy_count
|
||||
<< " updated=" << (desktop_policy.updated ? 1 : 0)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
@@ -20,7 +21,9 @@ Status MetadataError(const char* operation) {
|
||||
} // namespace
|
||||
|
||||
Status ConfigureSdlApplicationMetadata() {
|
||||
if (!SDL_SetAppMetadata(kMocktailApplicationName, MOCKTAIL_PROJECT_VERSION,
|
||||
const char* name=std::getenv("MOCKTAIL_VR_APPLICATION_NAME");
|
||||
if(!name || !name[0])name=kMocktailApplicationName;
|
||||
if (!SDL_SetAppMetadata(name, MOCKTAIL_PROJECT_VERSION,
|
||||
kMocktailApplicationIdentifier)) {
|
||||
return MetadataError("SDL_SetAppMetadata");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -17,6 +20,41 @@ namespace {
|
||||
constexpr std::size_t kNameSlotOffset = 8;
|
||||
constexpr std::size_t kRecordTailSize = 16;
|
||||
|
||||
struct ProcessMapping {
|
||||
std::uintptr_t first = 0, end = 0;
|
||||
bool writable = false;
|
||||
};
|
||||
|
||||
std::vector<ProcessMapping> ReadableMappings() {
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
std::vector<ProcessMapping> ranges;
|
||||
std::string line;
|
||||
while (std::getline(maps, line)) {
|
||||
unsigned long long first = 0, end = 0;
|
||||
char permissions[5]{};
|
||||
if (std::sscanf(line.c_str(), "%llx-%llx %4s", &first, &end, permissions) != 3 ||
|
||||
permissions[0] != 'r' || first >= end ||
|
||||
end > std::numeric_limits<std::uintptr_t>::max()) continue;
|
||||
ranges.push_back({static_cast<std::uintptr_t>(first),
|
||||
static_cast<std::uintptr_t>(end), permissions[1] == 'w'});
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
bool ReadableRange(const std::vector<ProcessMapping>& maps,
|
||||
std::uintptr_t address, std::size_t size) {
|
||||
if (!size || size > std::numeric_limits<std::uintptr_t>::max() - address)
|
||||
return false;
|
||||
const auto end = address + size;
|
||||
for (const auto& mapping : maps) {
|
||||
if (mapping.end <= address) continue;
|
||||
if (mapping.first > address) return false;
|
||||
address = std::min(end, mapping.end);
|
||||
if (address == end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string_view SegmentBytes(const LoadedImageSegment& segment) {
|
||||
return {reinterpret_cast<const char*>(segment.address), segment.size};
|
||||
}
|
||||
@@ -43,20 +81,41 @@ std::vector<LoadedImageSegment> LoadedImageSegments(std::uintptr_t image_base) {
|
||||
if (image_base == 0) {
|
||||
return segments;
|
||||
}
|
||||
const auto* header = reinterpret_cast<const Elf64_Ehdr*>(image_base);
|
||||
if (std::memcmp(header->e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
header->e_ident[EI_CLASS] != ELFCLASS64 || header->e_phoff == 0) {
|
||||
const auto mappings = ReadableMappings();
|
||||
if (!ReadableRange(mappings, image_base, sizeof(Elf64_Ehdr))) return segments;
|
||||
Elf64_Ehdr header{};
|
||||
std::memcpy(&header, reinterpret_cast<const void*>(image_base), sizeof(header));
|
||||
if (std::memcmp(header.e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
header.e_ident[EI_CLASS] != ELFCLASS64 || header.e_ident[EI_DATA] != ELFDATA2LSB ||
|
||||
header.e_phoff == 0 || header.e_phentsize != sizeof(Elf64_Phdr) ||
|
||||
header.e_phnum == 0 || header.e_phnum > 1024 ||
|
||||
header.e_phoff > std::numeric_limits<std::uintptr_t>::max() - image_base) {
|
||||
return segments;
|
||||
}
|
||||
const auto* program = reinterpret_cast<const Elf64_Phdr*>(
|
||||
image_base + static_cast<std::uintptr_t>(header->e_phoff));
|
||||
for (std::size_t index = 0; index < header->e_phnum; ++index) {
|
||||
if (program[index].p_type != PT_LOAD || program[index].p_memsz == 0) {
|
||||
const auto table = image_base + static_cast<std::uintptr_t>(header.e_phoff);
|
||||
if (!ReadableRange(mappings, table, header.e_phnum * sizeof(Elf64_Phdr)))
|
||||
return segments;
|
||||
for (std::size_t index = 0; index < header.e_phnum; ++index) {
|
||||
Elf64_Phdr program{};
|
||||
std::memcpy(&program, reinterpret_cast<const void*>(table + index * sizeof(program)), sizeof(program));
|
||||
if (program.p_type != PT_LOAD || program.p_memsz == 0) {
|
||||
continue;
|
||||
}
|
||||
segments.push_back({image_base + program[index].p_vaddr,
|
||||
static_cast<std::size_t>(program[index].p_memsz),
|
||||
(program[index].p_flags & PF_W) != 0});
|
||||
if (program.p_vaddr > std::numeric_limits<std::uintptr_t>::max() - image_base)
|
||||
return {};
|
||||
const auto first = image_base + program.p_vaddr;
|
||||
if (program.p_memsz > std::numeric_limits<std::uintptr_t>::max() - first)
|
||||
return {};
|
||||
const auto end = first + program.p_memsz;
|
||||
// PT_LOAD PF_W can include pages made read-only by GNU_RELRO. Only actual
|
||||
// readable mappings may be scanned, and actual writable pages may be edited.
|
||||
for (const auto& mapping : mappings) {
|
||||
const auto begin = std::max(first, mapping.first);
|
||||
const auto limit = std::min(end, mapping.end);
|
||||
if (begin < limit)
|
||||
segments.push_back({begin, static_cast<std::size_t>(limit - begin),
|
||||
(program.p_flags & PF_W) != 0 && mapping.writable});
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
@@ -70,13 +129,15 @@ std::uint8_t* FindFastFlagValue(const std::vector<LoadedImageSegment>& segments,
|
||||
std::uint8_t* found = nullptr;
|
||||
int matches = 0;
|
||||
for (const LoadedImageSegment& segment : segments) {
|
||||
if (!segment.writable || segment.size < kNameSlotOffset + kRecordTailSize) {
|
||||
if (!segment.writable || segment.size < kNameSlotOffset + kRecordTailSize ||
|
||||
segment.size > std::numeric_limits<std::uintptr_t>::max() - segment.address ||
|
||||
segment.address > std::numeric_limits<std::uintptr_t>::max() - kNameSlotOffset - 7) {
|
||||
continue;
|
||||
}
|
||||
const std::uintptr_t first =
|
||||
(segment.address + kNameSlotOffset + 7) & ~std::uintptr_t{7};
|
||||
const std::uintptr_t end = segment.address + segment.size;
|
||||
for (std::uintptr_t slot = first; slot + kRecordTailSize <= end;
|
||||
for (std::uintptr_t slot = first; slot <= end - kRecordTailSize;
|
||||
slot += 8) {
|
||||
std::uintptr_t pointer = 0;
|
||||
std::memcpy(&pointer, reinterpret_cast<const void*>(slot),
|
||||
@@ -94,7 +155,8 @@ std::uint8_t* FindFastFlagValue(const std::vector<LoadedImageSegment>& segments,
|
||||
++matches;
|
||||
}
|
||||
}
|
||||
return matches == 1 ? found : nullptr;
|
||||
return matches == 1 && __atomic_load_n(found, __ATOMIC_RELAXED) <= 1
|
||||
? found : nullptr;
|
||||
}
|
||||
|
||||
bool ClientSettingsSetLuauCodegenAll(std::string_view overrides_json) {
|
||||
@@ -112,6 +174,11 @@ LuauCodegenMenuSwitch::LuauCodegenMenuSwitch(
|
||||
LocateFlagFn locate_flag, RobloxExperiencePresenceObserver next)
|
||||
: locate_flag_(std::move(locate_flag)), next_(next) {}
|
||||
|
||||
bool LuauCodegenMenuSwitch::PrepareForMenus() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return SetEnabledLocked(true);
|
||||
}
|
||||
|
||||
RobloxExperiencePresenceObserver LuauCodegenMenuSwitch::observer() {
|
||||
return {this, &LuauCodegenMenuSwitch::Notify};
|
||||
}
|
||||
@@ -121,25 +188,33 @@ void LuauCodegenMenuSwitch::OnPhase(
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!located_) {
|
||||
located_ = true;
|
||||
flag_ = locate_flag_ ? locate_flag_() : nullptr;
|
||||
}
|
||||
if (flag_ != nullptr) {
|
||||
const std::uint8_t current = __atomic_load_n(flag_, __ATOMIC_RELAXED);
|
||||
const std::uint8_t wanted = LuauCodegenAllForPhase(phase) ? 1 : 0;
|
||||
if (current <= 1 && current != wanted) {
|
||||
__atomic_store_n(flag_, wanted, __ATOMIC_RELAXED);
|
||||
std::cout << " [luau-codegen] native code for all scripts "
|
||||
<< (wanted != 0 ? "on" : "off") << '\n';
|
||||
}
|
||||
}
|
||||
(void)SetEnabledLocked(LuauCodegenAllForPhase(phase));
|
||||
}
|
||||
if (next_.valid()) {
|
||||
next_.notify(next_.context, phase, request);
|
||||
}
|
||||
}
|
||||
|
||||
bool LuauCodegenMenuSwitch::SetEnabledLocked(bool enabled) {
|
||||
if (!located_) {
|
||||
located_ = true;
|
||||
flag_ = locate_flag_ ? locate_flag_() : nullptr;
|
||||
}
|
||||
if (flag_ == nullptr) return false;
|
||||
const std::uint8_t current = __atomic_load_n(flag_, __ATOMIC_RELAXED);
|
||||
if (current > 1) {
|
||||
flag_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
const std::uint8_t wanted = enabled ? 1 : 0;
|
||||
if (current != wanted) {
|
||||
__atomic_store_n(flag_, wanted, __ATOMIC_RELAXED);
|
||||
std::cout << " [luau-codegen] native code for all scripts "
|
||||
<< (wanted != 0 ? "on" : "off") << '\n';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void LuauCodegenMenuSwitch::Notify(
|
||||
void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
|
||||
@@ -377,7 +377,8 @@ bool MergeAudioDeviceMenuClientSettingsOverrides(std::string_view base_json,
|
||||
|
||||
bool MergeVrClientSettingsOverrides(bool enabled, std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error) {
|
||||
std::string* error,
|
||||
bool preserve_temporal_depth) {
|
||||
if (merged_json == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "VR client-settings output is required";
|
||||
@@ -411,6 +412,10 @@ bool MergeVrClientSettingsOverrides(bool enabled, std::string_view base_json,
|
||||
// is enabled; VR controllers connect as Gamepad1.
|
||||
overrides["FFlagKeepHapticsOnMobileWhenGamepadConnected"] = "True";
|
||||
overrides["FFlagVRHaptics"] = "True";
|
||||
if (preserve_temporal_depth) {
|
||||
// Memoryless allocations cannot preserve depth for temporal upscaling.
|
||||
overrides["FFlagVulkanLazilyAllocatedTextureSupport"] = "False";
|
||||
}
|
||||
}
|
||||
*merged_json = overrides.dump();
|
||||
return true;
|
||||
|
||||
Loaded 100 of 341 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user