mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 23:00:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d982cd9506 | ||
|
|
65adb3bddc | ||
|
|
47e23df2e0 | ||
|
|
c0ff1842ee | ||
|
|
e073947b91 | ||
|
|
66de662e5b | ||
|
|
6c3631058c | ||
|
|
6829fd00ed | ||
|
|
8be3618cd5 | ||
|
|
265b476440 | ||
|
|
ff905fe5b7 | ||
|
|
02108a9bb1 | ||
|
|
7dd32335ca | ||
|
|
5ed6ed5e84 | ||
|
|
cef1b3ec4a | ||
|
|
854969457f |
No files matched your search
+16
-74
@@ -1,8 +1,6 @@
|
||||
name: Publish AUR packages
|
||||
name: Check AUR packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -13,8 +11,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish AUR packages
|
||||
metadata:
|
||||
name: Refresh AUR metadata
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
@@ -27,10 +25,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
package_dir="$GITHUB_WORKSPACE/packaging/aur/mocktail-git"
|
||||
version="$(sed -n \
|
||||
's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' \
|
||||
CMakeLists.txt | head -n 1)"
|
||||
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"
|
||||
@@ -39,76 +36,21 @@ jobs:
|
||||
--volume "$package_dir:/pkg" \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
useradd --create-home builder
|
||||
install -d -o builder -g builder /tmp/mocktail-git
|
||||
install -d -o builder -g builder /tmp/nightcap-git
|
||||
install -o builder -g builder -m 0644 \
|
||||
/pkg/PKGBUILD /tmp/mocktail-git/PKGBUILD
|
||||
/pkg/PKGBUILD /tmp/nightcap-git/PKGBUILD
|
||||
runuser -u builder -- bash -c \
|
||||
"cd /tmp/mocktail-git && makepkg --printsrcinfo" \
|
||||
"cd /tmp/nightcap-git && makepkg --printsrcinfo" \
|
||||
> /pkg/.SRCINFO
|
||||
'
|
||||
|
||||
grep -Fqx "pkgver=${pkgver}" "$package_dir/PKGBUILD"
|
||||
grep -Fqx $'\tpkgver = '"${pkgver}" "$package_dir/.SRCINFO"
|
||||
printf 'AUR_PKGVER=%s\n' "$pkgver" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Publish to the AUR
|
||||
shell: bash
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
AUR_SSH_KEY_PASSPHRASE: ${{ secrets.AUR_SSH_KEY_PASSPHRASE }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ -n "$AUR_SSH_PRIVATE_KEY" ]]
|
||||
[[ -n "$AUR_SSH_KEY_PASSPHRASE" ]]
|
||||
|
||||
private_key="$RUNNER_TEMP/aur_ed25519"
|
||||
askpass="$RUNNER_TEMP/aur_askpass"
|
||||
known_hosts="$RUNNER_TEMP/aur_known_hosts"
|
||||
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$private_key"
|
||||
unset AUR_SSH_PRIVATE_KEY
|
||||
chmod 0600 "$private_key"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf '\''%s\n'\'' "$AUR_SSH_KEY_PASSPHRASE"' \
|
||||
> "$askpass"
|
||||
chmod 0700 "$askpass"
|
||||
eval "$(ssh-agent -s)"
|
||||
trap 'ssh-agent -k >/dev/null; rm -f -- "$private_key" "$askpass"' EXIT
|
||||
DISPLAY=:0 SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \
|
||||
ssh-add "$private_key" </dev/null
|
||||
unset AUR_SSH_KEY_PASSPHRASE
|
||||
|
||||
ssh-keyscan -T 10 -t ed25519 aur.archlinux.org > "$known_hosts"
|
||||
fingerprint="$(ssh-keygen -lf "$known_hosts" -E sha256 | awk '{print $2}')"
|
||||
[[ "$fingerprint" == \
|
||||
'SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4' ]]
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -F /dev/null -i $private_key \
|
||||
-o BatchMode=yes -o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile=$known_hosts"
|
||||
publish_aur_package() {
|
||||
local package_name="$1"
|
||||
local commit_message="$2"
|
||||
local source_dir="packaging/aur/$package_name"
|
||||
local aur_dir="$RUNNER_TEMP/${package_name}-aur"
|
||||
|
||||
git clone --branch master --single-branch \
|
||||
"ssh://aur@aur.archlinux.org/${package_name}.git" "$aur_dir"
|
||||
install -m 0644 "$source_dir/PKGBUILD" "$aur_dir/PKGBUILD"
|
||||
install -m 0644 "$source_dir/.SRCINFO" "$aur_dir/.SRCINFO"
|
||||
|
||||
git -C "$aur_dir" config user.name komaruworld
|
||||
git -C "$aur_dir" config user.email rtysus228@gmail.com
|
||||
git -C "$aur_dir" add PKGBUILD .SRCINFO
|
||||
if git -C "$aur_dir" diff --cached --quiet; then
|
||||
printf '%s is already published\n' "$package_name"
|
||||
return
|
||||
fi
|
||||
git -C "$aur_dir" commit -m "$commit_message"
|
||||
git -C "$aur_dir" push origin HEAD:master
|
||||
}
|
||||
|
||||
publish_aur_package mocktail-git "Update to ${AUR_PKGVER}"
|
||||
publish_aur_package mocktail 'make builds stable'
|
||||
publish_aur_package mocktail-bin 'make builds stable'
|
||||
- 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
|
||||
@@ -126,10 +126,153 @@ jobs:
|
||||
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]
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
@@ -138,6 +281,13 @@ jobs:
|
||||
- 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:
|
||||
@@ -152,7 +302,28 @@ jobs:
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
mv release/Mocktail-x86_64.AppImage release/Nightcap-x86_64.AppImage
|
||||
(cd release && sha256sum Nightcap-x86_64.AppImage > Nightcap-x86_64.AppImage.sha256)
|
||||
# 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.
|
||||
@@ -164,5 +335,4 @@ jobs:
|
||||
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.AppImage \
|
||||
release/Nightcap-x86_64.AppImage.sha256
|
||||
"${uploads[@]}"
|
||||
@@ -177,3 +177,6 @@ lsan.log*
|
||||
# Test fixtures
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.zip
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.apk
|
||||
|
||||
# Working plans, not part of the shipped documentation
|
||||
/docs/plans/
|
||||
+57
-1
@@ -6,6 +6,61 @@ GitHub are taken from the matching section here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
@@ -135,7 +190,8 @@ First Nightcap release, forked from komaruworld/mocktail.
|
||||
- The AppImage build no longer fails on stale checksums for the slimmed
|
||||
Arch packages.
|
||||
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.0...HEAD
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.1...HEAD
|
||||
[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
|
||||
+22
-17
@@ -786,23 +786,29 @@ install(CODE "
|
||||
endif()
|
||||
")
|
||||
|
||||
set(MOCKTAIL_PACKAGE_NAMES mocktail mocktail-nightly nightcap nightcap-nightly)
|
||||
set(MOCKTAIL_PACKAGE_NAME "mocktail" CACHE STRING
|
||||
"Native package name"
|
||||
)
|
||||
set(MOCKTAIL_PACKAGE_VERSION "${PROJECT_VERSION}" CACHE STRING
|
||||
"Native package version"
|
||||
)
|
||||
set(MOCKTAIL_PACKAGE_VERSION_SUFFIX "" CACHE STRING
|
||||
"Native package version suffix"
|
||||
)
|
||||
if(NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail" AND
|
||||
NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
if(NOT MOCKTAIL_PACKAGE_NAME IN_LIST MOCKTAIL_PACKAGE_NAMES)
|
||||
string(JOIN ", " MOCKTAIL_PACKAGE_NAMES_TEXT ${MOCKTAIL_PACKAGE_NAMES})
|
||||
message(FATAL_ERROR
|
||||
"MOCKTAIL_PACKAGE_NAME must be mocktail or mocktail-nightly")
|
||||
"MOCKTAIL_PACKAGE_NAME must be one of ${MOCKTAIL_PACKAGE_NAMES_TEXT}")
|
||||
endif()
|
||||
|
||||
set(CPACK_PACKAGE_NAME "${MOCKTAIL_PACKAGE_NAME}")
|
||||
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}")
|
||||
set(CPACK_PACKAGE_VENDOR "Mocktail Project")
|
||||
set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
|
||||
set(CPACK_PACKAGE_VERSION
|
||||
"${MOCKTAIL_PACKAGE_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}"
|
||||
)
|
||||
set(CPACK_PACKAGE_VENDOR "Nightcap")
|
||||
set(CPACK_PACKAGE_CONTACT "vii <coderdayton14@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/CoderDayton/nightcap")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION
|
||||
"Runs the Android x86-64 Roblox client on Linux."
|
||||
@@ -812,7 +818,7 @@ set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
|
||||
set(CPACK_STRIP_FILES TRUE)
|
||||
|
||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "vii <coderdayton14@gmail.com>")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
@@ -822,15 +828,14 @@ set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
|
||||
|
||||
if(MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail")
|
||||
else()
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail-nightly")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
endif()
|
||||
# Every variant installs the same /usr/bin/mocktail and /usr/share/mocktail,
|
||||
# so only one of them can be present at a time.
|
||||
set(MOCKTAIL_PACKAGE_SIBLINGS ${MOCKTAIL_PACKAGE_NAMES})
|
||||
list(REMOVE_ITEM MOCKTAIL_PACKAGE_SIBLINGS "${MOCKTAIL_PACKAGE_NAME}")
|
||||
string(JOIN ", " MOCKTAIL_PACKAGE_CONFLICTS ${MOCKTAIL_PACKAGE_SIBLINGS})
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
include(CPack)
|
||||
|
||||
add_dependencies(mocktail all_stubs)
|
||||
|
||||
@@ -61,10 +61,24 @@ chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Or build and install it from source using the steps under
|
||||
[Building](#building). There are no AUR, DEB, or RPM packages of Nightcap.
|
||||
Upstream ships those for unmodified Mocktail, see
|
||||
[komaruworld/mocktail](https://github.com/komaruworld/mocktail#readme).
|
||||
## 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>-1_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. Nightcap is not in any distribution repository or the AUR; the
|
||||
recipes under `packaging/aur` are kept for local builds only.
|
||||
|
||||
Or build and install from source using the steps under
|
||||
[Building](#building).
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -116,8 +130,9 @@ 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, minizip,
|
||||
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
|
||||
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.
|
||||
|
||||
@@ -128,7 +143,7 @@ See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instru
|
||||
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 libelf-dev libminizip-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
|
||||
@@ -140,7 +155,7 @@ sudo apt install build-essential cmake git ninja-build pkg-config lld \
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
curl openssl nlohmann-json libyaml libpng libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
libplacebo vulkan-headers vulkan-icd-loader zlib
|
||||
```
|
||||
@@ -152,7 +167,8 @@ sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
```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 elfutils-libelf-devel minizip-ng-compat-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
|
||||
|
||||
+13
-1
@@ -44,7 +44,8 @@ Each slice is named after the call it times, on the thread that made it.
|
||||
| `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 before a submit | `uploads`, `compressed_bytes`, `decoded_bytes` |
|
||||
| `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` |
|
||||
| `pipeline` | `vkCreateGraphicsPipelines`, `vkCreateComputePipelines` | `count`, `cache` (1 when a pipeline cache was passed), `result` |
|
||||
| `pipeline` | `vkCreateShaderModule` | `bytes`, `result` |
|
||||
| `pipeline` | `vkCreatePipelineCache` | `initial_bytes`, `result` |
|
||||
@@ -61,6 +62,17 @@ Run every scenario before and after the change, on the same build type
|
||||
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 |
|
||||
|
||||
+48
-2
@@ -27,20 +27,48 @@ request. If GameMode narrowed it, Nightcap ends the request and prints:
|
||||
```
|
||||
|
||||
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:
|
||||
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
|
||||
@@ -59,9 +87,20 @@ to the affinity. `off` never contacts the daemon.
|
||||
To see which CPUs the process may use:
|
||||
|
||||
```bash
|
||||
taskset -pc $(pgrep -x mocktail)
|
||||
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,
|
||||
@@ -99,6 +138,13 @@ 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -47,6 +47,14 @@ inline constexpr std::uint32_t kSmallTextureMaxExtent = 64;
|
||||
inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4;
|
||||
std::uint32_t SmallTextureUpscale(const char* value);
|
||||
|
||||
// A timeline semaphore and the value an asynchronous decode will signal.
|
||||
// `semaphore` is VK_NULL_HANDLE when the decode already finished on the
|
||||
// calling thread and the submit needs no extra wait.
|
||||
struct Etc2SubmitWait {
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
std::uint64_t value = 0;
|
||||
};
|
||||
|
||||
// Reports ETC2/EAC support on hosts without native ETC2 (desktop GPUs) and
|
||||
// stores those textures decoded. Images are created with the host format;
|
||||
// each staged upload into one is redirected to a host-visible staging buffer
|
||||
@@ -66,10 +74,14 @@ class VulkanEtc2Emulation final {
|
||||
bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device,
|
||||
PFN_vkGetPhysicalDeviceFeatures host);
|
||||
|
||||
// `timeline_semaphore` states whether the device was created with the
|
||||
// timelineSemaphore feature enabled. Without it every decode runs on the
|
||||
// calling thread.
|
||||
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
|
||||
bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr,
|
||||
bool timeline_semaphore = false);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
|
||||
@@ -112,12 +124,27 @@ class VulkanEtc2Emulation final {
|
||||
const VkCommandBuffer* secondaries);
|
||||
|
||||
// Decodes every upload recorded into these command buffers and the
|
||||
// secondary command buffers they execute. Large batches decode on worker
|
||||
// threads; this returns once every upload is decoded.
|
||||
void PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count);
|
||||
// secondary command buffers they execute.
|
||||
//
|
||||
// With `allow_async` on a device that has a timeline semaphore, the decode
|
||||
// runs on worker threads, and so does the gather of the compressed bytes
|
||||
// whenever the application already has them mapped. The returned semaphore
|
||||
// and value must then be added to the submit's waits, so the GPU copy
|
||||
// cannot execute before the decode finishes. Values are signalled in the
|
||||
// order they are handed out.
|
||||
//
|
||||
// Those threads read the application's source bytes after this returns,
|
||||
// which Vulkan allows: the application may not write them between the
|
||||
// submit and the copy's completion. Unmapping or freeing that memory waits
|
||||
// for the decodes still reading it.
|
||||
//
|
||||
// Returns `{VK_NULL_HANDLE, 0}` when there was nothing to decode, when
|
||||
// `allow_async` is false, or when the device has no timeline semaphore. In
|
||||
// those cases every upload is already decoded when this returns.
|
||||
Etc2SubmitWait PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count, bool allow_async = false);
|
||||
// Destroys the staging buffers owned by a command buffer that is no longer
|
||||
// pending.
|
||||
// pending, after waiting for any decode still writing into them.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
struct State;
|
||||
|
||||
@@ -152,6 +152,10 @@ struct MouseMotionEvent {
|
||||
float delta_x = 0.0f;
|
||||
float delta_y = 0.0f;
|
||||
std::uint32_t buttons = 0;
|
||||
// True while the window holds relative mouse mode. The absolute coordinates
|
||||
// are meaningless then: X11 reports zeroes, Wayland reports a position that
|
||||
// accumulates the deltas and can leave the window entirely.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct MouseButtonEvent {
|
||||
@@ -160,6 +164,8 @@ struct MouseButtonEvent {
|
||||
std::uint8_t clicks = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
// See MouseMotionEvent::relative.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct MouseWheelEvent {
|
||||
@@ -167,6 +173,8 @@ struct MouseWheelEvent {
|
||||
float delta_y = 0.0f;
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
// See MouseMotionEvent::relative.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct TouchEvent {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_text_display_state.h"
|
||||
|
||||
struct TTF_Font;
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
@@ -39,6 +41,8 @@ class RobloxTextSurfaceOverlay final {
|
||||
const RobloxTextDisplayUpdate& update);
|
||||
void ApplyUpdate(const RobloxTextDisplayUpdate& update);
|
||||
Status RasterizeLocked();
|
||||
Status OpenFontsLocked(std::int32_t font, float point_size);
|
||||
void CloseFontsLocked();
|
||||
void ClearFrameLocked();
|
||||
void RecordFailureLocked(Status status);
|
||||
|
||||
@@ -46,10 +50,14 @@ class RobloxTextSurfaceOverlay final {
|
||||
RobloxTextDisplayState state_;
|
||||
RobloxTextOverlayViewport viewport_;
|
||||
std::vector<std::uint8_t> rgba_;
|
||||
std::vector<TTF_Font*> fonts_;
|
||||
std::int32_t fonts_font_ = 0;
|
||||
float fonts_point_size_ = 0.0F;
|
||||
std::uint64_t state_revision_ = 0;
|
||||
std::uint64_t raster_revision_ = 0;
|
||||
Status failure_;
|
||||
bool initialized_ = false;
|
||||
bool ttf_initialized_ = false;
|
||||
bool failure_logged_ = false;
|
||||
bool ready_logged_ = false;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Maintainer: komaruworld <rtysus228@gmail.com>
|
||||
# Maintainer: vii <coderdayton14@gmail.com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.3
|
||||
pkgrel=2
|
||||
pkgname=nightcap
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail')
|
||||
depends=(
|
||||
'capstone'
|
||||
'curl'
|
||||
@@ -16,6 +18,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
'minizip'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail-bin
|
||||
pkgbase = nightcap-bin
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt)
|
||||
pkgver = 1.0.4
|
||||
pkgver = 0.3.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
depends = capstone
|
||||
@@ -17,6 +17,7 @@ pkgbase = mocktail-bin
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
@@ -29,12 +30,14 @@ pkgbase = mocktail-bin
|
||||
depends = zlib
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
provides = mocktail
|
||||
provides = nightcap
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-git
|
||||
noextract = mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
conflicts = nightcap
|
||||
conflicts = nightcap-git
|
||||
noextract = nightcap-0.3.1-1-x86_64.pkg.tar.zst
|
||||
options = !debug
|
||||
options = !strip
|
||||
source = mocktail-1.0.4-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.4/mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
sha256sums = d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7
|
||||
source = nightcap-0.3.1-1-x86_64.pkg.tar.zst::https://github.com/CoderDayton/nightcap/releases/download/v0.3.1/nightcap-0.3.1-1-x86_64.pkg.tar.zst
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail-bin
|
||||
pkgname = nightcap-bin
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-bin
|
||||
pkgver=1.0.4
|
||||
pkgname=nightcap-bin
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
@@ -32,17 +33,18 @@ depends=(
|
||||
'zlib'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-git')
|
||||
provides=('mocktail' 'nightcap')
|
||||
conflicts=('mocktail' 'nightcap' 'nightcap-git')
|
||||
options=('!debug' '!strip')
|
||||
# Release asset names do not include the AUR package release.
|
||||
_archive="mocktail-${pkgver}-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/komaruworld/mocktail/releases/download/${pkgver}/${_archive}")
|
||||
# The release asset carries the pacman package release built by release.yml.
|
||||
_archive="nightcap-${pkgver}-1-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/CoderDayton/nightcap/releases/download/v${pkgver}/${_archive}")
|
||||
noextract=("${_archive}")
|
||||
sha256sums=('d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7')
|
||||
# Replace with the checksum published beside the release asset.
|
||||
sha256sums=('SKIP')
|
||||
|
||||
package() {
|
||||
bsdtar -xf "${srcdir}/${_archive}" -C "${pkgdir}" usr
|
||||
install -Dm644 "${pkgdir}/usr/share/licenses/mocktail/LICENSE" \
|
||||
install -Dm644 "${pkgdir}/usr/share/licenses/nightcap/LICENSE" \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail
|
||||
pkgbase = nightcap-git
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.4
|
||||
pkgver = 0.3.1.r0.g0000000
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
makedepends = cmake>=3.20
|
||||
@@ -24,6 +24,7 @@ pkgbase = mocktail
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
@@ -35,11 +36,13 @@ pkgbase = mocktail
|
||||
depends = webkitgtk-6.0
|
||||
depends = zlib
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
conflicts = mocktail-bin
|
||||
conflicts = mocktail-git
|
||||
provides = mocktail
|
||||
provides = nightcap
|
||||
conflicts = mocktail
|
||||
conflicts = nightcap
|
||||
conflicts = nightcap-bin
|
||||
options = !debug
|
||||
options = !lto
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.4
|
||||
source = nightcap::git+https://github.com/CoderDayton/nightcap.git#branch=main
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
pkgname = nightcap-git
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-git
|
||||
pkgver=1.0.2.r24.g9bdd737
|
||||
pkgname=nightcap-git
|
||||
pkgver=0.3.1.r0.g0000000
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
@@ -41,31 +42,30 @@ makedepends=(
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-bin')
|
||||
provides=('mocktail' 'nightcap')
|
||||
conflicts=('mocktail' 'nightcap' 'nightcap-bin')
|
||||
options=('!debug')
|
||||
source=(
|
||||
'mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main'
|
||||
'nightcap::git+https://github.com/CoderDayton/nightcap.git#branch=main'
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
pkgver() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
local version
|
||||
version="$(sed -n \
|
||||
's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' \
|
||||
CMakeLists.txt | head -n 1)"
|
||||
version="$(git describe --tags --abbrev=0 --match='v[0-9]*' 2>/dev/null)"
|
||||
version="${version#v}"
|
||||
printf '%s.r%s.g%s' \
|
||||
"${version}" \
|
||||
"${version:-0.0.0}" \
|
||||
"$(git rev-list --count HEAD)" \
|
||||
"$(git rev-parse --short=7 HEAD)"
|
||||
}
|
||||
|
||||
prepare() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
@@ -73,7 +73,7 @@ prepare() {
|
||||
}
|
||||
|
||||
build() {
|
||||
cmake -S mocktail -B build -G Ninja \
|
||||
cmake -S nightcap -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
@@ -86,6 +86,6 @@ build() {
|
||||
|
||||
package() {
|
||||
DESTDIR="${pkgdir}" cmake --install build
|
||||
install -Dm644 mocktail/LICENSE \
|
||||
install -Dm644 nightcap/LICENSE \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail-git
|
||||
pkgbase = nightcap
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.2.r24.g9bdd737
|
||||
pkgver = 0.3.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
makedepends = cmake>=3.20
|
||||
@@ -24,6 +24,7 @@ pkgbase = mocktail-git
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
@@ -37,9 +38,11 @@ pkgbase = mocktail-git
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
provides = mocktail
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-bin
|
||||
conflicts = nightcap-bin
|
||||
conflicts = nightcap-git
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main
|
||||
options = !lto
|
||||
source = nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v0.3.1
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail-git
|
||||
pkgname = nightcap
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.4
|
||||
pkgname=nightcap
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
@@ -41,17 +42,18 @@ makedepends=(
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
conflicts=('mocktail-bin' 'mocktail-git')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'nightcap-bin' 'nightcap-git')
|
||||
options=('!debug' '!lto')
|
||||
source=(
|
||||
"mocktail::git+https://github.com/komaruworld/mocktail.git#tag=${pkgver}"
|
||||
"nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v${pkgver}"
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
prepare() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
@@ -59,7 +61,7 @@ prepare() {
|
||||
}
|
||||
|
||||
build() {
|
||||
cmake -S mocktail -B build -G Ninja \
|
||||
cmake -S nightcap -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
@@ -72,6 +74,6 @@ build() {
|
||||
|
||||
package() {
|
||||
DESTDIR="${pkgdir}" cmake --install build
|
||||
install -Dm644 mocktail/LICENSE \
|
||||
install -Dm644 nightcap/LICENSE \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -21,8 +21,9 @@
|
||||
Sign in to Roblox, browse experiences, and play them on Linux.
|
||||
</p>
|
||||
<p>
|
||||
Mocktail is an independent community project. It is not affiliated with
|
||||
Roblox Corporation or VinegarHQ, and the Roblox client is not included.
|
||||
Nightcap is an independent community project, forked from Mocktail. It
|
||||
is not affiliated with Roblox Corporation or VinegarHQ, and the Roblox
|
||||
client is not included.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
@@ -56,45 +57,49 @@
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="1.0.4" date="2026-09-11">
|
||||
<release version="0.3.1" date="2026-09-20">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed login issues</li>
|
||||
<li>Fixed low-resolution textures, skybox rendering and HiDPI scaling.</li>
|
||||
<li>Added vc support.</li>
|
||||
<li>Added Xbox and PlayStation gamepad support.</li>
|
||||
<li>Added Nix support</li>
|
||||
<li>Improved text input, fullscreen mode, window sizing, and mouse controls.</li>
|
||||
<li>Updated support for Roblox 2.736.1408.</li>
|
||||
<li>Added DEB, RPM, and pacman packages to every release.</li>
|
||||
<li>Smoother frames: textures no longer decode on the thread that submits the frame.</li>
|
||||
<li>Fixed the mouse pointer jumping when a right-click camera drag ended.</li>
|
||||
<li>Fixed the text caret not moving when a trailing space was typed.</li>
|
||||
<li>Faster typing in text boxes.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.3" date="2026-08-16">
|
||||
<release version="0.3.0" date="2026-09-18">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed issues with downloading Roblox on first launch</li>
|
||||
<li>Added fflags.json for custom FFlags</li>
|
||||
<li>Added a message if your cookies have expired</li>
|
||||
<li>Added a custom Discord presence: set the title, state, and icons.</li>
|
||||
<li>Added --profile to record a trace of the renderer's work.</li>
|
||||
<li>Faster texture loading: compressed textures decode and resample on worker threads.</li>
|
||||
<li>Lower CPU use while playing.</li>
|
||||
<li>Fixed GameMode pinning the game to a few CPU cores.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.2" date="2026-08-14">
|
||||
<release version="0.2.0" date="2026-09-16">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed the “Roblox is out of date” bug</li>
|
||||
<li>Improved logging</li>
|
||||
<li>Fixed issues with Discord RPC where the game you were playing wasn’t displayed</li>
|
||||
<li>Discord Rich Presence is on by default.</li>
|
||||
<li>Added input.raw_mouse for mouse look in the host's own units.</li>
|
||||
<li>Fixed mouse look after a long turn.</li>
|
||||
<li>Fixed a first-run failure with "cannot lock appStorage".</li>
|
||||
<li>Values in fflags.json now win over the performance preset.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.1" date="2026-08-13">
|
||||
<release version="0.1.0" date="2026-09-15">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fix issues with GameMode</li>
|
||||
<li>Add Discord RPC support</li>
|
||||
<li>First Nightcap release, forked from Mocktail.</li>
|
||||
<li>Added ETC2 and EAC texture emulation on Vulkan.</li>
|
||||
<li>Added PNG texture overrides.</li>
|
||||
<li>Lower input latency.</li>
|
||||
<li>Roblox now sees the real host RAM and display size.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-08-12"/>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -23,6 +23,7 @@
|
||||
elfutils,
|
||||
libglvnd,
|
||||
libplacebo,
|
||||
libpng,
|
||||
libsoup_3,
|
||||
libutf8proc,
|
||||
libyaml,
|
||||
@@ -92,6 +93,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
elfutils
|
||||
libglvnd
|
||||
libplacebo
|
||||
libpng
|
||||
libsoup_3
|
||||
libutf8proc
|
||||
libyaml
|
||||
|
||||
@@ -119,6 +119,7 @@ libyaml
|
||||
fontconfig
|
||||
minizip
|
||||
libplacebo
|
||||
libpng
|
||||
util-linux'
|
||||
INSTALL_ARGS='-S --needed --noconfirm'
|
||||
;;
|
||||
@@ -144,6 +145,7 @@ libyaml
|
||||
fontconfig
|
||||
minizip
|
||||
libplacebo
|
||||
libpng
|
||||
util-linux'
|
||||
if HostUsesMusl; then
|
||||
PACKAGES="$PACKAGES gcompat"
|
||||
@@ -172,6 +174,7 @@ libyaml
|
||||
fontconfig
|
||||
minizip
|
||||
libplacebo
|
||||
libpng
|
||||
coreutils
|
||||
findutils
|
||||
util-linux'
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Prints the native package version for the current ref. A release tag gives
|
||||
# its own version; anything else gives 0.0.0, which sorts below every release.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
ref="${1:-${GITHUB_REF_NAME:-}}"
|
||||
version="${ref#v}"
|
||||
|
||||
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
printf '%s\n' "${version}"
|
||||
else
|
||||
printf '0.0.0\n'
|
||||
fi
|
||||
@@ -22,6 +22,10 @@
|
||||
<p>Flatpak</p>
|
||||
<code>flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap</code>
|
||||
<p>DEB, RPM and pacman</p>
|
||||
<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>
|
||||
<p>Build from source</p>
|
||||
<code>git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap && make build
|
||||
@@ -40,6 +44,7 @@ cd nightcap && make build
|
||||
<p class="links">
|
||||
<a href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage">AppImage</a>
|
||||
<a href="Nightcap-x86_64.flatpak">Flatpak bundle</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap/releases/latest">DEB, RPM, pacman</a>
|
||||
<a href="nightcap-flatpak.gpg">Signing key</a>
|
||||
<a href="terms.html">Terms</a>
|
||||
<a href="privacy.html">Privacy</a>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -1300,18 +1301,134 @@ void ReleaseEtc2PoolCommandBuffers(VkDevice device,
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits) {
|
||||
if (submits == nullptr) {
|
||||
return;
|
||||
// The stage the decoded staging bytes are read at. Every emulated upload is
|
||||
// a buffer-to-image copy, so the transfer stage is the earliest point the GPU
|
||||
// may touch them.
|
||||
constexpr VkPipelineStageFlags kEtc2WaitStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||
|
||||
// Holds the arrays a rewritten submit points at, so they outlive the call.
|
||||
// Deques keep earlier elements addressable as later ones are appended.
|
||||
struct Etc2SubmitRewrite {
|
||||
std::deque<std::vector<VkSemaphore>> wait_semaphores;
|
||||
std::deque<std::vector<VkPipelineStageFlags>> wait_stages;
|
||||
std::deque<std::vector<std::uint64_t>> wait_values;
|
||||
std::deque<std::vector<std::uint64_t>> signal_values;
|
||||
std::deque<VkTimelineSemaphoreSubmitInfo> timelines;
|
||||
std::deque<std::vector<VkSemaphoreSubmitInfo>> wait_infos;
|
||||
std::vector<VkSubmitInfo> rewritten;
|
||||
std::vector<VkSubmitInfo2> rewritten2;
|
||||
};
|
||||
|
||||
// Adds each submit's decode wait to its wait list. A submit whose pNext chain
|
||||
// already sizes arrays by its own semaphore counts is decoded on this thread
|
||||
// instead: a VkTimelineSemaphoreSubmitInfo would have to be rebuilt, and a
|
||||
// VkDeviceGroupSubmitInfo's pWaitSemaphoreDeviceIndices would be left one
|
||||
// entry short of the grown wait list.
|
||||
const VkSubmitInfo* PrepareEtc2Submit(std::uint32_t submit_count,
|
||||
const VkSubmitInfo* submits,
|
||||
Etc2SubmitRewrite* rewrite) {
|
||||
if (submits == nullptr || submit_count == 0) {
|
||||
return submits;
|
||||
}
|
||||
bool rewritten_any = false;
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
for (std::uint32_t info = 0; info < submits[index].commandBufferInfoCount;
|
||||
++info) {
|
||||
State().etc2.PrepareSubmit(
|
||||
&submits[index].pCommandBufferInfos[info].commandBuffer, 1);
|
||||
const VkSubmitInfo& original = submits[index];
|
||||
const bool fixed_wait_list =
|
||||
FindFeature(original.pNext,
|
||||
VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO) !=
|
||||
nullptr ||
|
||||
FindFeature(original.pNext,
|
||||
VK_STRUCTURE_TYPE_DEVICE_GROUP_SUBMIT_INFO) != nullptr;
|
||||
const mocktail::graphics::Etc2SubmitWait wait = State().etc2.PrepareSubmit(
|
||||
original.pCommandBuffers, original.commandBufferCount,
|
||||
!fixed_wait_list);
|
||||
if (wait.semaphore == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
if (!rewritten_any) {
|
||||
rewrite->rewritten.assign(submits, submits + submit_count);
|
||||
rewritten_any = true;
|
||||
}
|
||||
std::vector<VkSemaphore>& semaphores = rewrite->wait_semaphores.emplace_back(
|
||||
original.pWaitSemaphores,
|
||||
original.pWaitSemaphores + original.waitSemaphoreCount);
|
||||
semaphores.push_back(wait.semaphore);
|
||||
std::vector<VkPipelineStageFlags>& stages =
|
||||
rewrite->wait_stages.emplace_back(
|
||||
original.pWaitDstStageMask,
|
||||
original.pWaitDstStageMask + original.waitSemaphoreCount);
|
||||
stages.push_back(kEtc2WaitStage);
|
||||
// Binary semaphores ignore their value; only the appended entry is read.
|
||||
std::vector<std::uint64_t>& values = rewrite->wait_values.emplace_back(
|
||||
original.waitSemaphoreCount, 0);
|
||||
values.push_back(wait.value);
|
||||
|
||||
VkTimelineSemaphoreSubmitInfo& timeline =
|
||||
rewrite->timelines.emplace_back();
|
||||
timeline.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO;
|
||||
timeline.pNext = original.pNext;
|
||||
timeline.waitSemaphoreValueCount = static_cast<std::uint32_t>(values.size());
|
||||
timeline.pWaitSemaphoreValues = values.data();
|
||||
if (original.signalSemaphoreCount != 0) {
|
||||
std::vector<std::uint64_t>& signals =
|
||||
rewrite->signal_values.emplace_back(original.signalSemaphoreCount, 0);
|
||||
timeline.signalSemaphoreValueCount =
|
||||
static_cast<std::uint32_t>(signals.size());
|
||||
timeline.pSignalSemaphoreValues = signals.data();
|
||||
}
|
||||
|
||||
VkSubmitInfo& target = rewrite->rewritten[index];
|
||||
target.pNext = &timeline;
|
||||
target.waitSemaphoreCount = static_cast<std::uint32_t>(semaphores.size());
|
||||
target.pWaitSemaphores = semaphores.data();
|
||||
target.pWaitDstStageMask = stages.data();
|
||||
}
|
||||
return rewritten_any ? rewrite->rewritten.data() : submits;
|
||||
}
|
||||
|
||||
const VkSubmitInfo2* PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits,
|
||||
Etc2SubmitRewrite* rewrite) {
|
||||
if (submits == nullptr || submit_count == 0) {
|
||||
return submits;
|
||||
}
|
||||
bool rewritten_any = false;
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
const VkSubmitInfo2& original = submits[index];
|
||||
std::vector<VkCommandBuffer> command_buffers;
|
||||
command_buffers.reserve(original.commandBufferInfoCount);
|
||||
for (std::uint32_t info = 0; info < original.commandBufferInfoCount;
|
||||
++info) {
|
||||
command_buffers.push_back(
|
||||
original.pCommandBufferInfos[info].commandBuffer);
|
||||
}
|
||||
const mocktail::graphics::Etc2SubmitWait wait = State().etc2.PrepareSubmit(
|
||||
command_buffers.data(),
|
||||
static_cast<std::uint32_t>(command_buffers.size()), true);
|
||||
if (wait.semaphore == VK_NULL_HANDLE) {
|
||||
continue;
|
||||
}
|
||||
if (!rewritten_any) {
|
||||
rewrite->rewritten2.assign(submits, submits + submit_count);
|
||||
rewritten_any = true;
|
||||
}
|
||||
std::vector<VkSemaphoreSubmitInfo>& waits = rewrite->wait_infos.emplace_back(
|
||||
original.pWaitSemaphoreInfos,
|
||||
original.pWaitSemaphoreInfos + original.waitSemaphoreInfoCount);
|
||||
VkSemaphoreSubmitInfo decoded{};
|
||||
decoded.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
|
||||
decoded.semaphore = wait.semaphore;
|
||||
decoded.value = wait.value;
|
||||
// Matches kEtc2WaitStage: the staging bytes are read by transfer
|
||||
// commands, which includes the blits an upscaled upload records.
|
||||
decoded.stageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT;
|
||||
waits.push_back(decoded);
|
||||
|
||||
VkSubmitInfo2& target = rewrite->rewritten2[index];
|
||||
target.waitSemaphoreInfoCount = static_cast<std::uint32_t>(waits.size());
|
||||
target.pWaitSemaphoreInfos = waits.data();
|
||||
}
|
||||
return rewritten_any ? rewrite->rewritten2.data() : submits;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1664,7 +1781,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
host_memory_properties(physical_device, &memory_properties);
|
||||
}
|
||||
State().etc2.RegisterDevice(*device, physical_device, etc2_emulated,
|
||||
memory_properties, host_get_device_proc_addr);
|
||||
memory_properties, host_get_device_proc_addr,
|
||||
enabled.vulkan12.timelineSemaphore == VK_TRUE);
|
||||
|
||||
std::uint32_t queue_family_count = 0;
|
||||
const auto host_get_queue_families =
|
||||
@@ -2143,14 +2261,11 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
if (submits != nullptr) {
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
State().etc2.PrepareSubmit(submits[index].pCommandBuffers,
|
||||
submits[index].commandBufferCount);
|
||||
}
|
||||
}
|
||||
Etc2SubmitRewrite rewrite;
|
||||
const VkSubmitInfo* prepared =
|
||||
PrepareEtc2Submit(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
queue, submit_count, prepared, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2168,9 +2283,11 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit2", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
Etc2SubmitRewrite rewrite;
|
||||
const VkSubmitInfo2* prepared =
|
||||
PrepareEtc2Submit2(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
queue, submit_count, prepared, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
}
|
||||
@@ -2188,9 +2305,11 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit2KHR", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
Etc2SubmitRewrite rewrite;
|
||||
const VkSubmitInfo2* prepared =
|
||||
PrepareEtc2Submit2(submit_count, submits, &rewrite);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence,
|
||||
queue, submit_count, prepared, fence,
|
||||
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -157,6 +159,13 @@ struct HostDevice {
|
||||
PFN_vkCreateBuffer create_buffer = nullptr;
|
||||
PFN_vkAllocateMemory allocate_memory = nullptr;
|
||||
PFN_vkGetBufferMemoryRequirements get_buffer_memory_requirements = nullptr;
|
||||
PFN_vkCmdPipelineBarrier pipeline_barrier = nullptr;
|
||||
PFN_vkSignalSemaphore signal_semaphore = nullptr;
|
||||
PFN_vkDestroySemaphore destroy_semaphore = nullptr;
|
||||
// Signalled by the decode dispatcher so a submit can wait for its uploads
|
||||
// on the GPU instead of on the submitting thread. Null when the device was
|
||||
// created without the timelineSemaphore feature.
|
||||
VkSemaphore timeline = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
template <typename Function>
|
||||
@@ -215,6 +224,29 @@ struct Staging {
|
||||
// texture uploads would otherwise stall the frame that records it.
|
||||
constexpr VkDeviceSize kMaxIdleStagingBytes = 64 * 1024 * 1024;
|
||||
constexpr std::size_t kMaxIdleStagingBuffers = 64;
|
||||
// Decode batches kept for their buffers once they complete.
|
||||
constexpr std::size_t kMaxSpareBatches = 8;
|
||||
|
||||
// A growable byte buffer that leaves new bytes uninitialized. The gather
|
||||
// writes every byte it later reads, and zeroing a multi-megabyte buffer is
|
||||
// itself frame-visible work.
|
||||
class RawBuffer {
|
||||
public:
|
||||
void EnsureSize(std::size_t bytes) {
|
||||
if (bytes <= size_) {
|
||||
return;
|
||||
}
|
||||
// new[] on a trivial type default-initializes, so nothing is zeroed.
|
||||
data_.reset(new std::uint8_t[bytes]);
|
||||
size_ = bytes;
|
||||
}
|
||||
std::uint8_t* data() { return data_.get(); }
|
||||
std::size_t size() const { return size_; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<std::uint8_t[]> data_;
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
// A request may take an idle buffer up to this size, or up to 4x its own
|
||||
// size when larger, so small uploads share buffers without pinning big ones.
|
||||
constexpr VkDeviceSize kStagingReuseSlack = 64 * 1024;
|
||||
@@ -257,6 +289,32 @@ struct CommandRecord {
|
||||
std::vector<PendingUpload> uploads;
|
||||
std::vector<Staging> staging;
|
||||
std::vector<VkCommandBuffer> secondaries;
|
||||
// Highest decode ticket dispatched for this command buffer. Its staging
|
||||
// must not be released before that ticket completes.
|
||||
std::uint64_t last_ticket = 0;
|
||||
};
|
||||
|
||||
// One submit's worth of uploads, decoded off the submitting thread. The
|
||||
// buffers and the upload list are owned so nothing in them depends on the
|
||||
// caller's stack.
|
||||
struct DecodeBatch {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
std::uint64_t ticket = 0;
|
||||
RawBuffer source;
|
||||
std::vector<std::uint8_t> decoded;
|
||||
// Bytes each buffer must hold. Both are grown on the dispatcher, since
|
||||
// sizing them on the submitting thread puts that cost back on the frame.
|
||||
// `decoded` stays a zeroed vector so a failed decode leaves defined bytes
|
||||
// rather than whatever the allocator handed back.
|
||||
VkDeviceSize source_bytes = 0;
|
||||
VkDeviceSize decoded_bytes = 0;
|
||||
std::vector<PendingUpload> uploads;
|
||||
// The application's mapped pointer for each upload, parallel to `uploads`.
|
||||
// Non-empty when the gather was deferred to the dispatcher, which Vulkan
|
||||
// allows: the application may not write these bytes between the submit and
|
||||
// the copy's completion. Unmapping or freeing that memory drains pending
|
||||
// decodes first, so the pointers stay dereferenceable.
|
||||
std::vector<const std::uint8_t*> deferred_sources;
|
||||
};
|
||||
|
||||
std::atomic<unsigned> g_image_logs{0};
|
||||
@@ -327,6 +385,55 @@ bool CreateStaging(const HostDevice& dev, VkDeviceSize size, Staging* staging,
|
||||
}
|
||||
|
||||
// Freeing mapped memory unmaps it implicitly.
|
||||
// Packs each upload's compressed bytes into `destination`, dropping the row
|
||||
// and layer padding the application may have asked for. Sources are parallel
|
||||
// to `uploads`.
|
||||
void GatherCompressed(const std::vector<PendingUpload>& uploads,
|
||||
const std::vector<const std::uint8_t*>& sources,
|
||||
std::uint8_t* destination) {
|
||||
VkDeviceSize offset = 0;
|
||||
for (std::size_t index = 0; index < uploads.size(); ++index) {
|
||||
const PendingUpload& upload = uploads[index];
|
||||
const std::uint8_t* source = sources[index];
|
||||
std::uint8_t* target = destination + offset;
|
||||
const VkDeviceSize compressed_layer = upload.compressed / upload.layers;
|
||||
if (upload.source_span == upload.compressed) {
|
||||
std::memcpy(target, source, upload.compressed);
|
||||
} else {
|
||||
const VkDeviceSize block_rows =
|
||||
(static_cast<VkDeviceSize>(upload.height) + 3) / 4;
|
||||
const VkDeviceSize packed_row = compressed_layer / block_rows;
|
||||
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
|
||||
for (VkDeviceSize row = 0; row < block_rows; ++row) {
|
||||
std::memcpy(target + layer * compressed_layer + row * packed_row,
|
||||
source + layer * upload.source_layer_stride +
|
||||
row * upload.source_row_stride,
|
||||
packed_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += upload.compressed;
|
||||
}
|
||||
}
|
||||
|
||||
// Makes the decode's host writes to staging visible to the transfer that
|
||||
// reads them. A queue submission performs the host-to-device domain operation
|
||||
// only for writes that happened before it, and an asynchronous decode writes
|
||||
// after the submit, so the dependency has to be recorded explicitly.
|
||||
void RecordHostWriteBarrier(const HostDevice& dev,
|
||||
VkCommandBuffer command_buffer) {
|
||||
if (dev.pipeline_barrier == nullptr) {
|
||||
return;
|
||||
}
|
||||
VkMemoryBarrier barrier{};
|
||||
barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
||||
barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
dev.pipeline_barrier(command_buffer, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &barrier, 0,
|
||||
nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
void DestroyStaging(const HostDevice& dev, const Staging& staging) {
|
||||
if (dev.destroy_buffer != nullptr) {
|
||||
dev.destroy_buffer(dev.device, staging.buffer, nullptr);
|
||||
@@ -509,7 +616,7 @@ struct VulkanEtc2Emulation::State {
|
||||
VkDeviceSize idle_staging_bytes = 0;
|
||||
// Heap buffers reused across submits; they only grow.
|
||||
std::mutex scratch_mutex;
|
||||
std::vector<std::uint8_t> scratch_source;
|
||||
RawBuffer scratch_source;
|
||||
std::vector<std::uint8_t> scratch_decoded;
|
||||
TextureOverrides overrides = TextureOverrides::FromEnvironment();
|
||||
const std::uint32_t upscale =
|
||||
@@ -745,11 +852,203 @@ struct VulkanEtc2Emulation::State {
|
||||
record.staging.push_back(staging);
|
||||
has_commands.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Asynchronous decode. One dispatcher runs batches in the order tickets
|
||||
// were handed out, so the timeline semaphore is always signalled with an
|
||||
// increasing value and a waiter never observes a later batch's signal.
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable queue_ready;
|
||||
std::condition_variable queue_done;
|
||||
std::deque<std::unique_ptr<DecodeBatch>> queue;
|
||||
std::vector<std::unique_ptr<DecodeBatch>> spare_batches;
|
||||
std::uint64_t issued_ticket = 0;
|
||||
std::uint64_t completed_ticket = 0;
|
||||
bool stopping = false;
|
||||
std::thread dispatcher;
|
||||
|
||||
// Recycles a batch's buffers; they only grow, as the scratch buffers do.
|
||||
std::unique_ptr<DecodeBatch> AcquireBatch() {
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
if (spare_batches.empty()) {
|
||||
return std::make_unique<DecodeBatch>();
|
||||
}
|
||||
std::unique_ptr<DecodeBatch> batch = std::move(spare_batches.back());
|
||||
spare_batches.pop_back();
|
||||
batch->uploads.clear();
|
||||
return batch;
|
||||
}
|
||||
|
||||
void StartDispatcher() {
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
if (dispatcher.joinable() || stopping) {
|
||||
return;
|
||||
}
|
||||
dispatcher = std::thread([this] { RunDispatcher(); });
|
||||
}
|
||||
|
||||
// Hands out the next ticket and queues the batch behind it.
|
||||
std::uint64_t Enqueue(std::unique_ptr<DecodeBatch> batch) {
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
batch->ticket = ++issued_ticket;
|
||||
const std::uint64_t ticket = batch->ticket;
|
||||
queue.push_back(std::move(batch));
|
||||
queue_ready.notify_one();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
void RunDispatcher() {
|
||||
for (;;) {
|
||||
std::unique_ptr<DecodeBatch> batch;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
queue_ready.wait(lock, [this] { return stopping || !queue.empty(); });
|
||||
if (queue.empty()) {
|
||||
if (stopping) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
batch = std::move(queue.front());
|
||||
queue.pop_front();
|
||||
}
|
||||
RunBatch(*batch);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
completed_ticket = batch->ticket;
|
||||
if (spare_batches.size() < kMaxSpareBatches) {
|
||||
spare_batches.push_back(std::move(batch));
|
||||
}
|
||||
queue_done.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RunBatch(DecodeBatch& batch) {
|
||||
TraceScope trace_scope(ActiveProfileTrace(), "etc2 decode", "texture");
|
||||
trace_scope.Arg("uploads", static_cast<std::int64_t>(batch.uploads.size()));
|
||||
trace_scope.Arg("compressed_bytes",
|
||||
static_cast<std::int64_t>(batch.source_bytes));
|
||||
trace_scope.Arg("decoded_bytes",
|
||||
static_cast<std::int64_t>(batch.decoded_bytes));
|
||||
batch.source.EnsureSize(batch.source_bytes);
|
||||
if (batch.decoded.size() < batch.decoded_bytes) {
|
||||
batch.decoded.resize(batch.decoded_bytes);
|
||||
}
|
||||
if (!batch.deferred_sources.empty()) {
|
||||
GatherCompressed(batch.uploads, batch.deferred_sources,
|
||||
batch.source.data());
|
||||
batch.deferred_sources.clear();
|
||||
}
|
||||
DecodeAndEmit(batch.uploads, batch.source.data(), batch.decoded.data());
|
||||
SignalDecoded(batch.device, batch.ticket);
|
||||
}
|
||||
|
||||
void SignalDecoded(VkDevice device, std::uint64_t value) {
|
||||
const HostDevice* dev = Find(device);
|
||||
if (dev == nullptr || dev->timeline == VK_NULL_HANDLE ||
|
||||
dev->signal_semaphore == nullptr) {
|
||||
return;
|
||||
}
|
||||
VkSemaphoreSignalInfo info{};
|
||||
info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO;
|
||||
info.semaphore = dev->timeline;
|
||||
info.value = value;
|
||||
if (dev->signal_semaphore(dev->device, &info) != VK_SUCCESS) {
|
||||
LogFailure("could not signal the decode timeline semaphore");
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes every layer of every upload and writes the host texels. Shared by
|
||||
// the dispatcher and by the synchronous path.
|
||||
void DecodeAndEmit(const std::vector<PendingUpload>& uploads,
|
||||
std::uint8_t* source_base, std::uint8_t* decoded_base) {
|
||||
std::vector<EtcDecodeJob> jobs;
|
||||
VkDeviceSize compressed_offset = 0;
|
||||
VkDeviceSize decoded_offset = 0;
|
||||
for (const PendingUpload& upload : uploads) {
|
||||
std::uint8_t* source = source_base + compressed_offset;
|
||||
std::uint8_t* decoded = decoded_base + decoded_offset;
|
||||
const VkDeviceSize compressed_layer = upload.compressed / upload.layers;
|
||||
const VkDeviceSize decoded_layer = upload.decoded / upload.layers;
|
||||
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
|
||||
EtcDecodeJob job;
|
||||
job.format = upload.format;
|
||||
job.source = source + layer * compressed_layer;
|
||||
job.source_bytes = compressed_layer;
|
||||
job.width = upload.width;
|
||||
job.height = upload.height;
|
||||
job.destination = decoded + layer * decoded_layer;
|
||||
job.destination_bytes = decoded_layer;
|
||||
jobs.push_back(job);
|
||||
}
|
||||
compressed_offset += upload.compressed;
|
||||
decoded_offset += upload.decoded;
|
||||
if (ShouldLog(&g_decode_logs, 4)) {
|
||||
std::fprintf(stderr, " [vulkan] ETC2 upload decoded %ux%u layers=%u\n",
|
||||
upload.width, upload.height, upload.layers);
|
||||
}
|
||||
}
|
||||
DecodeEtcJobs(jobs.data(), jobs.size(), DecodeWorkerCount());
|
||||
for (const EtcDecodeJob& job : jobs) {
|
||||
if (!job.ok) {
|
||||
LogFailure("could not decode an upload layer");
|
||||
}
|
||||
}
|
||||
compressed_offset = 0;
|
||||
decoded_offset = 0;
|
||||
std::vector<EmitJob> emit_jobs;
|
||||
emit_jobs.reserve(uploads.size());
|
||||
for (const PendingUpload& upload : uploads) {
|
||||
emit_jobs.push_back({&upload, source_base + compressed_offset,
|
||||
decoded_base + decoded_offset});
|
||||
compressed_offset += upload.compressed;
|
||||
decoded_offset += upload.decoded;
|
||||
}
|
||||
EmitBatch(std::move(emit_jobs), DecodeWorkerCount());
|
||||
}
|
||||
|
||||
// Blocks until every batch up to `ticket` has been decoded and emitted.
|
||||
void WaitForTicket(std::uint64_t ticket) {
|
||||
if (ticket == 0) {
|
||||
return;
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
queue_done.wait(lock, [this, ticket] {
|
||||
return completed_ticket >= ticket || stopping;
|
||||
});
|
||||
}
|
||||
|
||||
// Blocks until nothing is queued or in flight.
|
||||
void DrainDecodes() {
|
||||
std::uint64_t ticket = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
ticket = issued_ticket;
|
||||
}
|
||||
WaitForTicket(ticket);
|
||||
}
|
||||
|
||||
void StopDispatcher() {
|
||||
std::thread worker;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex);
|
||||
stopping = true;
|
||||
queue_ready.notify_all();
|
||||
queue_done.notify_all();
|
||||
worker = std::move(dispatcher);
|
||||
}
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VulkanEtc2Emulation::VulkanEtc2Emulation() : state_(new State) {}
|
||||
|
||||
VulkanEtc2Emulation::~VulkanEtc2Emulation() { delete state_; }
|
||||
VulkanEtc2Emulation::~VulkanEtc2Emulation() {
|
||||
state_->StopDispatcher();
|
||||
delete state_;
|
||||
}
|
||||
|
||||
bool VulkanEtc2Emulation::PhysicalDeviceNeedsEmulation(
|
||||
VkPhysicalDevice physical_device, PFN_vkGetPhysicalDeviceFeatures host) {
|
||||
@@ -775,7 +1074,7 @@ bool VulkanEtc2Emulation::PhysicalDeviceNeedsEmulation(
|
||||
void VulkanEtc2Emulation::RegisterDevice(
|
||||
VkDevice device, VkPhysicalDevice physical_device, bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr) {
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr, bool timeline_semaphore) {
|
||||
static_cast<void>(physical_device);
|
||||
if (device == VK_NULL_HANDLE || get_device_proc_addr == nullptr) {
|
||||
return;
|
||||
@@ -821,6 +1120,31 @@ void VulkanEtc2Emulation::RegisterDevice(
|
||||
dev->get_buffer_memory_requirements =
|
||||
DeviceProc<PFN_vkGetBufferMemoryRequirements>(
|
||||
get, device, "vkGetBufferMemoryRequirements");
|
||||
dev->pipeline_barrier =
|
||||
DeviceProc<PFN_vkCmdPipelineBarrier>(get, device, "vkCmdPipelineBarrier");
|
||||
dev->signal_semaphore = DeviceProc<PFN_vkSignalSemaphore>(
|
||||
get, device, "vkSignalSemaphore", "vkSignalSemaphoreKHR");
|
||||
dev->destroy_semaphore =
|
||||
DeviceProc<PFN_vkDestroySemaphore>(get, device, "vkDestroySemaphore");
|
||||
if (emulated && timeline_semaphore && dev->signal_semaphore != nullptr) {
|
||||
const auto create_semaphore =
|
||||
DeviceProc<PFN_vkCreateSemaphore>(get, device, "vkCreateSemaphore");
|
||||
VkSemaphoreTypeCreateInfo type{};
|
||||
type.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
|
||||
type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
|
||||
type.initialValue = 0;
|
||||
VkSemaphoreCreateInfo info{};
|
||||
info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
|
||||
info.pNext = &type;
|
||||
if (create_semaphore != nullptr &&
|
||||
create_semaphore(device, &info, nullptr, &dev->timeline) !=
|
||||
VK_SUCCESS) {
|
||||
dev->timeline = VK_NULL_HANDLE;
|
||||
}
|
||||
if (dev->timeline != VK_NULL_HANDLE) {
|
||||
state_->StartDispatcher();
|
||||
}
|
||||
}
|
||||
if (emulated) {
|
||||
std::fprintf(stderr,
|
||||
" [vulkan] ETC2/EAC emulation enabled: compressed uploads "
|
||||
@@ -837,6 +1161,8 @@ void VulkanEtc2Emulation::RegisterDevice(
|
||||
}
|
||||
|
||||
void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
// Nothing may free staging or the semaphore while a decode still writes it.
|
||||
state_->DrainDecodes();
|
||||
std::vector<Staging> doomed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
@@ -872,6 +1198,9 @@ void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
for (const Staging& staging : doomed) {
|
||||
DestroyStaging(*dev, staging);
|
||||
}
|
||||
if (dev->timeline != VK_NULL_HANDLE && dev->destroy_semaphore != nullptr) {
|
||||
dev->destroy_semaphore(device, dev->timeline, nullptr);
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->devices_mutex);
|
||||
state_->devices.erase(
|
||||
@@ -1035,6 +1364,9 @@ void VulkanEtc2Emulation::UnmapMemory(VkDevice device, VkDeviceMemory memory) {
|
||||
return;
|
||||
}
|
||||
if (dev->emulated) {
|
||||
// A deferred gather reads this mapping on a worker, so it must finish
|
||||
// before the pointer stops being valid.
|
||||
state_->DrainDecodes();
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->mappings.erase(memory);
|
||||
}
|
||||
@@ -1048,6 +1380,7 @@ VkResult VulkanEtc2Emulation::UnmapMemory2(VkDevice device,
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
if (dev->emulated && info != nullptr) {
|
||||
state_->DrainDecodes();
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->mappings.erase(info->memory);
|
||||
}
|
||||
@@ -1061,6 +1394,7 @@ void VulkanEtc2Emulation::FreeMemory(VkDevice device, VkDeviceMemory memory,
|
||||
return;
|
||||
}
|
||||
if (dev->emulated) {
|
||||
state_->DrainDecodes();
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->mappings.erase(memory);
|
||||
}
|
||||
@@ -1111,6 +1445,7 @@ void VulkanEtc2Emulation::CmdCopyBufferToImage(
|
||||
upload.target = staging.mapped + upload.target_offset;
|
||||
}
|
||||
state_->Record(command_buffer, std::move(uploads), staging);
|
||||
RecordHostWriteBarrier(*dev, command_buffer);
|
||||
dev->copy_buffer_to_image(command_buffer, staging.buffer, destination,
|
||||
layout, region_count, rewritten.data());
|
||||
}
|
||||
@@ -1146,6 +1481,7 @@ void VulkanEtc2Emulation::CmdCopyBufferToImage2(
|
||||
upload.target = staging.mapped + upload.target_offset;
|
||||
}
|
||||
state_->Record(command_buffer, std::move(uploads), staging);
|
||||
RecordHostWriteBarrier(*dev, command_buffer);
|
||||
VkCopyBufferToImageInfo2 host_info = *info;
|
||||
host_info.srcBuffer = staging.buffer;
|
||||
host_info.pRegions = rewritten.data();
|
||||
@@ -1261,11 +1597,12 @@ void VulkanEtc2Emulation::CmdExecuteCommands(
|
||||
dev->execute_commands(command_buffer, count, secondaries);
|
||||
}
|
||||
|
||||
void VulkanEtc2Emulation::PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count) {
|
||||
Etc2SubmitWait VulkanEtc2Emulation::PrepareSubmit(
|
||||
const VkCommandBuffer* command_buffers, std::uint32_t count,
|
||||
bool allow_async) {
|
||||
if (command_buffers == nullptr || count == 0 ||
|
||||
!state_->has_commands.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
struct Work {
|
||||
PendingUpload upload;
|
||||
@@ -1323,8 +1660,26 @@ void VulkanEtc2Emulation::PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
}
|
||||
}
|
||||
}
|
||||
TraceScope trace_scope(work.empty() ? nullptr : ActiveProfileTrace(),
|
||||
"etc2 decode", "texture");
|
||||
if (work.empty()) {
|
||||
return {};
|
||||
}
|
||||
// A submit carries one device, but a batch that somehow spans two cannot be
|
||||
// signalled by a single timeline, so it decodes on this thread instead.
|
||||
VkDevice batch_device = work.front().upload.device;
|
||||
bool single_device = true;
|
||||
for (const Work& item : work) {
|
||||
if (item.upload.device != batch_device) {
|
||||
single_device = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const HostDevice* batch_dev = state_->Find(batch_device);
|
||||
const bool async = allow_async && single_device && batch_dev != nullptr &&
|
||||
batch_dev->timeline != VK_NULL_HANDLE &&
|
||||
batch_dev->signal_semaphore != nullptr;
|
||||
|
||||
TraceScope trace_scope(ActiveProfileTrace(),
|
||||
async ? "etc2 gather" : "etc2 decode", "texture");
|
||||
// Mapped Vulkan memory is uncached or write-combined, which makes the
|
||||
// decoder's scattered byte reads and writes tens of times slower than on
|
||||
// heap memory. Compressed bytes are streamed into a heap scratch buffer,
|
||||
@@ -1377,89 +1732,110 @@ void VulkanEtc2Emulation::PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
static_cast<std::int64_t>(compressed_total));
|
||||
trace_scope.Arg("decoded_bytes", static_cast<std::int64_t>(decoded_total));
|
||||
|
||||
std::lock_guard<std::mutex> scratch_lock(state_->scratch_mutex);
|
||||
std::vector<std::uint8_t>& scratch_source = state_->scratch_source;
|
||||
std::vector<std::uint8_t>& scratch_decoded = state_->scratch_decoded;
|
||||
if (scratch_source.size() < compressed_total) {
|
||||
scratch_source.resize(compressed_total);
|
||||
std::unique_ptr<DecodeBatch> batch;
|
||||
if (async) {
|
||||
batch = state_->AcquireBatch();
|
||||
batch->device = batch_device;
|
||||
}
|
||||
if (scratch_decoded.size() < decoded_total) {
|
||||
scratch_decoded.resize(decoded_total);
|
||||
// Owned copies, so the decode never reads the caller's stack.
|
||||
std::vector<PendingUpload> uploads;
|
||||
uploads.reserve(copied_uploads.size());
|
||||
for (const PendingUpload* upload : copied_uploads) {
|
||||
uploads.push_back(*upload);
|
||||
}
|
||||
std::vector<EtcDecodeJob> jobs;
|
||||
VkDeviceSize compressed_offset = 0;
|
||||
VkDeviceSize decoded_offset = 0;
|
||||
for (std::size_t index = 0; index < copies.size(); ++index) {
|
||||
const Copy& copy = copies[index];
|
||||
const PendingUpload& upload = *copied_uploads[index];
|
||||
std::uint8_t* source = scratch_source.data() + compressed_offset;
|
||||
std::uint8_t* decoded = scratch_decoded.data() + decoded_offset;
|
||||
const VkDeviceSize compressed_layer = upload.compressed / upload.layers;
|
||||
if (upload.source_span == copy.compressed) {
|
||||
std::memcpy(source, copy.source, copy.compressed);
|
||||
} else {
|
||||
// Padded rows: gather the covered blocks into the packed layout the
|
||||
// decoder expects.
|
||||
const VkDeviceSize block_rows =
|
||||
(static_cast<VkDeviceSize>(upload.height) + 3) / 4;
|
||||
const VkDeviceSize packed_row = compressed_layer / block_rows;
|
||||
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
|
||||
for (VkDeviceSize row = 0; row < block_rows; ++row) {
|
||||
std::memcpy(source + layer * compressed_layer + row * packed_row,
|
||||
copy.source + layer * upload.source_layer_stride +
|
||||
row * upload.source_row_stride,
|
||||
packed_row);
|
||||
}
|
||||
std::vector<const std::uint8_t*> sources;
|
||||
sources.reserve(copies.size());
|
||||
for (const Copy& copy : copies) {
|
||||
sources.push_back(copy.source);
|
||||
}
|
||||
|
||||
// Reading the application's staging means reading write-combined memory,
|
||||
// which is slow enough to show in a frame. When nothing had to be mapped
|
||||
// for this batch, the application's own mapping stays valid until the copy
|
||||
// completes, so the gather moves to the dispatcher with the decode and the
|
||||
// submitting thread keeps only the bookkeeping.
|
||||
const bool defer_gather = async && temporary.empty();
|
||||
|
||||
if (!async) {
|
||||
std::lock_guard<std::mutex> scratch_lock(state_->scratch_mutex);
|
||||
state_->scratch_source.EnsureSize(compressed_total);
|
||||
if (state_->scratch_decoded.size() < decoded_total) {
|
||||
state_->scratch_decoded.resize(decoded_total);
|
||||
}
|
||||
GatherCompressed(uploads, sources, state_->scratch_source.data());
|
||||
for (const auto& [memory, mapping] : temporary) {
|
||||
if (mapping.dev->unmap_memory != nullptr) {
|
||||
mapping.dev->unmap_memory(mapping.dev->device, memory);
|
||||
}
|
||||
}
|
||||
const VkDeviceSize decoded_layer = upload.decoded / upload.layers;
|
||||
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
|
||||
EtcDecodeJob job;
|
||||
job.format = upload.format;
|
||||
job.source = source + layer * compressed_layer;
|
||||
job.source_bytes = compressed_layer;
|
||||
job.width = upload.width;
|
||||
job.height = upload.height;
|
||||
job.destination = decoded + layer * decoded_layer;
|
||||
job.destination_bytes = decoded_layer;
|
||||
jobs.push_back(job);
|
||||
}
|
||||
compressed_offset += copy.compressed;
|
||||
decoded_offset += copy.decoded;
|
||||
if (ShouldLog(&g_decode_logs, 4)) {
|
||||
std::fprintf(stderr,
|
||||
" [vulkan] ETC2 upload decoded %ux%u layers=%u\n",
|
||||
upload.width, upload.height, upload.layers);
|
||||
state_->DecodeAndEmit(uploads, state_->scratch_source.data(),
|
||||
state_->scratch_decoded.data());
|
||||
return {};
|
||||
}
|
||||
|
||||
batch->source_bytes = compressed_total;
|
||||
batch->decoded_bytes = decoded_total;
|
||||
if (defer_gather) {
|
||||
batch->deferred_sources = std::move(sources);
|
||||
} else {
|
||||
batch->source.EnsureSize(compressed_total);
|
||||
GatherCompressed(uploads, sources, batch->source.data());
|
||||
for (const auto& [memory, mapping] : temporary) {
|
||||
if (mapping.dev->unmap_memory != nullptr) {
|
||||
mapping.dev->unmap_memory(mapping.dev->device, memory);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& [memory, mapping] : temporary) {
|
||||
if (mapping.dev->unmap_memory != nullptr) {
|
||||
mapping.dev->unmap_memory(mapping.dev->device, memory);
|
||||
|
||||
batch->uploads = std::move(uploads);
|
||||
// The staging these uploads write into must outlive the decode, so the
|
||||
// command buffers that own it remember the ticket. The batch is queued
|
||||
// while the state lock is held, so a concurrent ReleaseCommandBuffer cannot
|
||||
// read a stale ticket and recycle staging the dispatcher is still writing.
|
||||
// Tickets only ever go up, so a record keeps the highest one it has seen.
|
||||
std::uint64_t ticket = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
ticket = state_->Enqueue(std::move(batch));
|
||||
const auto remember = [this, ticket](VkCommandBuffer command_buffer) {
|
||||
const auto record = state_->commands.find(command_buffer);
|
||||
if (record != state_->commands.end()) {
|
||||
record->second.last_ticket =
|
||||
std::max(record->second.last_ticket, ticket);
|
||||
}
|
||||
};
|
||||
for (std::uint32_t index = 0; index < count; ++index) {
|
||||
const auto record = state_->commands.find(command_buffers[index]);
|
||||
if (record == state_->commands.end()) {
|
||||
continue;
|
||||
}
|
||||
record->second.last_ticket =
|
||||
std::max(record->second.last_ticket, ticket);
|
||||
for (VkCommandBuffer secondary : record->second.secondaries) {
|
||||
remember(secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
DecodeEtcJobs(jobs.data(), jobs.size(), DecodeWorkerCount());
|
||||
for (const EtcDecodeJob& job : jobs) {
|
||||
if (!job.ok) {
|
||||
LogFailure("could not decode an upload layer");
|
||||
}
|
||||
}
|
||||
compressed_offset = 0;
|
||||
decoded_offset = 0;
|
||||
std::vector<State::EmitJob> emit_jobs;
|
||||
emit_jobs.reserve(copied_uploads.size());
|
||||
for (const PendingUpload* upload : copied_uploads) {
|
||||
emit_jobs.push_back({upload, scratch_source.data() + compressed_offset,
|
||||
scratch_decoded.data() + decoded_offset});
|
||||
compressed_offset += upload->compressed;
|
||||
decoded_offset += upload->decoded;
|
||||
}
|
||||
state_->EmitBatch(std::move(emit_jobs), DecodeWorkerCount());
|
||||
trace_scope.Arg("ticket", static_cast<std::int64_t>(ticket));
|
||||
return {batch_dev->timeline, ticket};
|
||||
}
|
||||
|
||||
void VulkanEtc2Emulation::ReleaseCommandBuffer(VkCommandBuffer command_buffer) {
|
||||
if (!state_->has_commands.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::uint64_t ticket = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
const auto record = state_->commands.find(command_buffer);
|
||||
if (record == state_->commands.end()) {
|
||||
return;
|
||||
}
|
||||
ticket = record->second.last_ticket;
|
||||
}
|
||||
// Waited on without holding the state lock, which the decode itself takes.
|
||||
state_->WaitForTicket(ticket);
|
||||
|
||||
std::vector<Staging> doomed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
|
||||
@@ -61,14 +61,17 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
|
||||
return true;
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
destination->payload = MouseMotionEvent{
|
||||
source.motion.x, source.motion.y, source.motion.xrel,
|
||||
source.motion.yrel, static_cast<std::uint32_t>(source.motion.state)};
|
||||
source.motion.x, source.motion.y,
|
||||
source.motion.xrel, source.motion.yrel,
|
||||
static_cast<std::uint32_t>(source.motion.state),
|
||||
SDL_GetWindowRelativeMouseMode(window)};
|
||||
return true;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
destination->payload = MouseButtonEvent{
|
||||
source.button.down, source.button.button, source.button.clicks,
|
||||
source.button.x, source.button.y};
|
||||
source.button.down, source.button.button, source.button.clicks,
|
||||
source.button.x, source.button.y,
|
||||
SDL_GetWindowRelativeMouseMode(window)};
|
||||
return true;
|
||||
case SDL_EVENT_MOUSE_WHEEL: {
|
||||
float delta_x = source.wheel.x;
|
||||
@@ -77,8 +80,10 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
|
||||
delta_x = -delta_x;
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
destination->payload = MouseWheelEvent{
|
||||
delta_x, delta_y, source.wheel.mouse_x, source.wheel.mouse_y};
|
||||
destination->payload =
|
||||
MouseWheelEvent{delta_x, delta_y, source.wheel.mouse_x,
|
||||
source.wheel.mouse_y,
|
||||
SDL_GetWindowRelativeMouseMode(window)};
|
||||
return true;
|
||||
}
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
|
||||
@@ -1242,6 +1242,7 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
if (active_launch_ != nullptr) {
|
||||
completed_result = active_launch_->result;
|
||||
game_active_ = completed_result.ok();
|
||||
window::SetGameSessionActive(game_active_);
|
||||
active_game_canonical_json_ =
|
||||
completed_result.ok() ? active_launch_->request.canonical_json
|
||||
: std::string();
|
||||
@@ -1279,6 +1280,7 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
return observed;
|
||||
}
|
||||
game_active_ = false;
|
||||
window::SetGameSessionActive(false);
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
@@ -1454,6 +1456,7 @@ Status RobloxExperienceComposition::LeaveGame() {
|
||||
if (status.ok()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
game_active_ = false;
|
||||
window::SetGameSessionActive(false);
|
||||
active_game_canonical_json_.clear();
|
||||
}
|
||||
return status;
|
||||
@@ -1552,6 +1555,7 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
subscribed_ = false;
|
||||
game_active_ = false;
|
||||
window::SetGameSessionActive(false);
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
|
||||
@@ -550,13 +550,14 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
|
||||
const float delta_x = transform.HostLogicalToGuestX(event.delta_x);
|
||||
const float delta_y = transform.HostLogicalToGuestY(event.delta_y);
|
||||
|
||||
// Relative-mode motion reports zero absolute coordinates and a delta. The
|
||||
// pointer holds its captured position while the delta passes through, so a
|
||||
// long camera turn neither parks the reported position at the viewport edge
|
||||
// The pointer holds its captured position while the delta passes through, so
|
||||
// a long camera turn neither parks the reported position at the viewport edge
|
||||
// nor swallows the reversal. Roblox draws its cursor at the held position
|
||||
// when the capture ends, which matches where the host pointer is warped.
|
||||
if (event.x == 0.0f && event.y == 0.0f &&
|
||||
(delta_x != 0.0f || delta_y != 0.0f)) {
|
||||
// Capture is read from the event, not inferred: only X11 zeroes the absolute
|
||||
// coordinates, while Wayland reports a position that accumulates the deltas.
|
||||
if (event.relative || (event.x == 0.0f && event.y == 0.0f &&
|
||||
(delta_x != 0.0f || delta_y != 0.0f))) {
|
||||
HoldPointerLocked(transform);
|
||||
} else {
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
@@ -595,9 +596,10 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked(
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
// Button events during pointer capture carry zero coordinates and must not
|
||||
// move the held position.
|
||||
if (event.x > 0.0f || event.y > 0.0f) {
|
||||
// Button events during pointer capture must not move the held position.
|
||||
// Their coordinates are zeroed on X11 and drifted on Wayland, so the capture
|
||||
// flag decides.
|
||||
if (!event.relative && (event.x > 0.0f || event.y > 0.0f)) {
|
||||
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.x), 0.0f, max_x);
|
||||
mouse_y_ = std::clamp(transform.HostLogicalToGuestY(event.y), 0.0f, max_y);
|
||||
pointer_anchored_ = true;
|
||||
@@ -638,9 +640,10 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseWheelLocked(
|
||||
}
|
||||
const platform::SurfaceCoordinateTransform transform =
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
// Wheel events during pointer capture carry zero coordinates and must not
|
||||
// move the held position.
|
||||
if (event.mouse_x > 0.0f || event.mouse_y > 0.0f) {
|
||||
// Wheel events during pointer capture must not move the held position.
|
||||
// Their coordinates are zeroed on X11 and drifted on Wayland, so the capture
|
||||
// flag decides.
|
||||
if (!event.relative && (event.mouse_x > 0.0f || event.mouse_y > 0.0f)) {
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.mouse_x), 0.0f,
|
||||
|
||||
@@ -244,7 +244,7 @@ struct TextBytePosition {
|
||||
};
|
||||
|
||||
TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset,
|
||||
int text_width) {
|
||||
int text_width, bool single_line) {
|
||||
if (layout == nullptr) {
|
||||
return {};
|
||||
}
|
||||
@@ -259,7 +259,11 @@ TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset,
|
||||
position.y = substring.rect.y;
|
||||
position.height = substring.rect.h;
|
||||
if ((substring.flags & TTF_SUBSTRING_TEXT_END) != 0) {
|
||||
position.x = std::min(text_width, substring.rect.x + substring.rect.w);
|
||||
// Trailing whitespace has no ink, so the final cluster rect stops short of
|
||||
// the laid-out width that a caret at the end of a single line sits behind.
|
||||
position.x = single_line ? text_width
|
||||
: std::min(text_width,
|
||||
substring.rect.x + substring.rect.w);
|
||||
}
|
||||
return position;
|
||||
}
|
||||
@@ -297,6 +301,7 @@ Status RobloxTextSurfaceOverlay::Shutdown() {
|
||||
}
|
||||
state_.Reset();
|
||||
ClearFrameLocked();
|
||||
CloseFontsLocked();
|
||||
viewport_ = {};
|
||||
initialized_ = false;
|
||||
if (owned_active_overlay) {
|
||||
@@ -441,10 +446,6 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
|
||||
const bool wrapped_layout =
|
||||
presentation.multiline || presentation.text_wrapped;
|
||||
|
||||
if (!TTF_Init()) {
|
||||
SecureClear(&candidate);
|
||||
return RasterizationFailure("unable to initialize SDL3_ttf");
|
||||
}
|
||||
const RobloxTextFontSelection font_selection =
|
||||
ResolveRobloxTextFont(presentation.font);
|
||||
const float native_point_size =
|
||||
@@ -454,23 +455,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
|
||||
? std::clamp(native_point_size * font_selection.size_scale, 1.0F,
|
||||
512.0F)
|
||||
: std::clamp(static_cast<float>(height) * 0.38F, 12.0F, 28.0F);
|
||||
const std::vector<std::string> font_files = ResolveFontFiles(font_selection);
|
||||
std::vector<TTF_Font*> fonts;
|
||||
fonts.reserve(font_files.size());
|
||||
for (const std::string& file : font_files) {
|
||||
TTF_Font* font = TTF_OpenFont(file.c_str(), point_size);
|
||||
if (font != nullptr) {
|
||||
fonts.push_back(font);
|
||||
}
|
||||
}
|
||||
if (fonts.empty()) {
|
||||
TTF_Quit();
|
||||
if (Status status = OpenFontsLocked(presentation.font, point_size);
|
||||
!status.ok()) {
|
||||
SecureClear(&candidate);
|
||||
return RasterizationFailure("unable to open a Roblox or fallback font");
|
||||
}
|
||||
for (std::size_t index = 1; index < fonts.size(); ++index) {
|
||||
(void)TTF_AddFallbackFont(fonts.front(), fonts[index]);
|
||||
return status;
|
||||
}
|
||||
const std::vector<TTF_Font*>& fonts = fonts_;
|
||||
if (wrapped_layout) {
|
||||
TTF_HorizontalAlignment alignment = TTF_HORIZONTAL_ALIGN_LEFT;
|
||||
if (presentation.x_alignment == 1) {
|
||||
@@ -501,11 +491,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
|
||||
TextBytePosition selection_end_position;
|
||||
if (layout != nullptr) {
|
||||
(void)TTF_GetTextSize(layout, &text_width, &text_height);
|
||||
caret_position = TextPosition(layout, caret_byte, text_width);
|
||||
selection_begin_position =
|
||||
TextPosition(layout, selection_begin_byte, text_width);
|
||||
caret_position =
|
||||
TextPosition(layout, caret_byte, text_width, !wrapped_layout);
|
||||
selection_begin_position = TextPosition(layout, selection_begin_byte,
|
||||
text_width, !wrapped_layout);
|
||||
selection_end_position =
|
||||
TextPosition(layout, selection_end_byte, text_width);
|
||||
TextPosition(layout, selection_end_byte, text_width, !wrapped_layout);
|
||||
}
|
||||
if (!text.value.empty()) {
|
||||
// SDL_ttf implementations have historically differed on whether fg.a is
|
||||
@@ -624,11 +615,6 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
|
||||
if (layout != nullptr) {
|
||||
TTF_DestroyText(layout);
|
||||
}
|
||||
TTF_ClearFallbackFonts(fonts.front());
|
||||
for (auto iterator = fonts.rbegin(); iterator != fonts.rend(); ++iterator) {
|
||||
TTF_CloseFont(*iterator);
|
||||
}
|
||||
TTF_Quit();
|
||||
|
||||
if (!status.ok()) {
|
||||
SecureClear(&candidate);
|
||||
@@ -646,6 +632,58 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxTextSurfaceOverlay::OpenFontsLocked(std::int32_t font,
|
||||
float point_size) {
|
||||
if (!fonts_.empty() && fonts_font_ == font &&
|
||||
fonts_point_size_ == point_size) {
|
||||
return Status::Ok();
|
||||
}
|
||||
if (!TTF_Init()) {
|
||||
return RasterizationFailure("unable to initialize SDL3_ttf");
|
||||
}
|
||||
const std::vector<std::string> files =
|
||||
ResolveFontFiles(ResolveRobloxTextFont(font));
|
||||
std::vector<TTF_Font*> opened;
|
||||
opened.reserve(files.size());
|
||||
for (const std::string& file : files) {
|
||||
TTF_Font* candidate = TTF_OpenFont(file.c_str(), point_size);
|
||||
if (candidate != nullptr) {
|
||||
opened.push_back(candidate);
|
||||
}
|
||||
}
|
||||
// A failed reopen leaves the fonts already rendering in place.
|
||||
if (opened.empty()) {
|
||||
TTF_Quit();
|
||||
return RasterizationFailure("unable to open a Roblox or fallback font");
|
||||
}
|
||||
for (std::size_t index = 1; index < opened.size(); ++index) {
|
||||
(void)TTF_AddFallbackFont(opened.front(), opened[index]);
|
||||
}
|
||||
CloseFontsLocked();
|
||||
fonts_ = std::move(opened);
|
||||
ttf_initialized_ = true;
|
||||
fonts_font_ = font;
|
||||
fonts_point_size_ = point_size;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
void RobloxTextSurfaceOverlay::CloseFontsLocked() {
|
||||
if (!fonts_.empty()) {
|
||||
TTF_ClearFallbackFonts(fonts_.front());
|
||||
for (auto iterator = fonts_.rbegin(); iterator != fonts_.rend();
|
||||
++iterator) {
|
||||
TTF_CloseFont(*iterator);
|
||||
}
|
||||
fonts_.clear();
|
||||
}
|
||||
fonts_font_ = 0;
|
||||
fonts_point_size_ = 0.0F;
|
||||
if (ttf_initialized_) {
|
||||
TTF_Quit();
|
||||
ttf_initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RobloxTextSurfaceOverlay::ClearFrameLocked() {
|
||||
SecureClear(&rgba_);
|
||||
raster_revision_ = 0;
|
||||
|
||||
+43
-9
@@ -151,13 +151,33 @@ static std::unique_ptr<WindowResizeReadinessGate> g_resize_readiness_gate;
|
||||
static std::unique_ptr<SdlTextInputBackend> g_text_input_backend;
|
||||
static std::unique_ptr<WindowTextInputOwner> g_text_input_owner;
|
||||
static std::unique_ptr<SdlPointerCaptureBackend> g_pointer_capture_backend;
|
||||
static std::unique_ptr<WindowPointerCaptureOwner> g_pointer_capture_owner;
|
||||
// The owner is created and destroyed on the SDL thread, but the experience
|
||||
// composition reaches it from its own thread. The mutex guards the pointer
|
||||
// only; a caller off the SDL thread holds a strong reference for its call.
|
||||
static std::mutex g_pointer_capture_owner_mutex;
|
||||
static std::shared_ptr<WindowPointerCaptureOwner> g_pointer_capture_owner;
|
||||
static std::unique_ptr<graphics::GlesTextOverlayCompositor> g_gles_text_overlay;
|
||||
static char g_preferred_egl_library[4096];
|
||||
static char g_preferred_gles_library[4096];
|
||||
static bool g_auto_angle_retry_attempted = false;
|
||||
static std::filesystem::path g_window_state_path;
|
||||
|
||||
// Keeps the owner alive past a concurrent Shutdown on the SDL thread.
|
||||
static std::shared_ptr<WindowPointerCaptureOwner> SharedPointerCaptureOwner() {
|
||||
std::lock_guard<std::mutex> lock(g_pointer_capture_owner_mutex);
|
||||
return g_pointer_capture_owner;
|
||||
}
|
||||
|
||||
// Drops the reference under the lock and destroys the owner outside it, so a
|
||||
// concurrent caller finishing its call cannot block the lock.
|
||||
static void ResetPointerCaptureOwner() {
|
||||
std::shared_ptr<WindowPointerCaptureOwner> owner;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_pointer_capture_owner_mutex);
|
||||
owner.swap(g_pointer_capture_owner);
|
||||
}
|
||||
}
|
||||
|
||||
WindowStartupPresentationPlan RestoredWindowPresentationPlan() {
|
||||
if (!g_state.state_persistence_active) {
|
||||
return {};
|
||||
@@ -865,8 +885,11 @@ bool ActivateWindowEventLifecycles() {
|
||||
std::make_unique<WindowTextInputOwner>(g_text_input_backend.get());
|
||||
g_pointer_capture_backend =
|
||||
std::make_unique<SdlPointerCaptureBackend>(g_state.sdl_window);
|
||||
g_pointer_capture_owner = std::make_unique<WindowPointerCaptureOwner>(
|
||||
g_pointer_capture_backend.get());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_pointer_capture_owner_mutex);
|
||||
g_pointer_capture_owner = std::make_shared<WindowPointerCaptureOwner>(
|
||||
g_pointer_capture_backend.get());
|
||||
}
|
||||
fprintf(stderr,
|
||||
" [input] SDL event pump target=%llu Hz raw-motion, no smoothing\n",
|
||||
static_cast<unsigned long long>(kProductionInputPumpHz));
|
||||
@@ -1484,13 +1507,24 @@ bool SetPlatformEventObserver(PlatformEventObserver observer, void* context) {
|
||||
void ClearPlatformEventObserver() { g_platform_event_observer.Clear(); }
|
||||
|
||||
bool SetMouseLockQueryCallback(MouseLockQueryCallback callback, void* context) {
|
||||
return g_pointer_capture_owner != nullptr &&
|
||||
g_pointer_capture_owner->RegisterQuery(callback, context);
|
||||
const std::shared_ptr<WindowPointerCaptureOwner> owner =
|
||||
SharedPointerCaptureOwner();
|
||||
return owner != nullptr && owner->RegisterQuery(callback, context);
|
||||
}
|
||||
|
||||
void SetGameSessionActive(bool active) {
|
||||
const std::shared_ptr<WindowPointerCaptureOwner> owner =
|
||||
SharedPointerCaptureOwner();
|
||||
if (owner != nullptr) {
|
||||
owner->SetGameSessionActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
void ClearMouseLockQueryCallback() {
|
||||
if (g_pointer_capture_owner != nullptr) {
|
||||
g_pointer_capture_owner->ClearQuery();
|
||||
const std::shared_ptr<WindowPointerCaptureOwner> owner =
|
||||
SharedPointerCaptureOwner();
|
||||
if (owner != nullptr) {
|
||||
owner->ClearQuery();
|
||||
}
|
||||
}
|
||||
bool SetPreTextInputPumpCallback(PreTextInputPumpCallback callback,
|
||||
@@ -2336,7 +2370,7 @@ void Shutdown() {
|
||||
if (!g_state.initialised) {
|
||||
g_fullscreen_request_gate.Reset();
|
||||
g_fullscreen_menu_request_gate.Reset();
|
||||
g_pointer_capture_owner.reset();
|
||||
ResetPointerCaptureOwner();
|
||||
g_pointer_capture_backend.reset();
|
||||
g_text_input_owner.reset();
|
||||
g_text_input_backend.reset();
|
||||
@@ -2355,7 +2389,7 @@ void Shutdown() {
|
||||
!g_pointer_capture_owner->Shutdown()) {
|
||||
fprintf(stderr, " [input] SDL pointer capture shutdown failed\n");
|
||||
}
|
||||
g_pointer_capture_owner.reset();
|
||||
ResetPointerCaptureOwner();
|
||||
g_pointer_capture_backend.reset();
|
||||
g_text_input_owner.reset();
|
||||
g_text_input_backend.reset();
|
||||
|
||||
@@ -83,6 +83,10 @@ void ClearPlatformEventObserver();
|
||||
bool SetMouseLockQueryCallback(MouseLockQueryCallback callback, void* context);
|
||||
void ClearMouseLockQueryCallback();
|
||||
|
||||
// Gates the RMB camera-drag fallback, which only stands in for a camera and
|
||||
// must not capture over the app surface's own UI.
|
||||
void SetGameSessionActive(bool active);
|
||||
|
||||
// Clear waits for a pre-input callback already in flight.
|
||||
bool SetPreTextInputPumpCallback(PreTextInputPumpCallback callback,
|
||||
void* context);
|
||||
|
||||
@@ -95,7 +95,7 @@ bool WindowPointerCaptureOwner::Pump(bool text_input_active) {
|
||||
}
|
||||
const bool client_active = query_ok;
|
||||
const bool native_capture_active = client_active && native_lock_active;
|
||||
const bool right_drag_active = right_button_held_;
|
||||
const bool right_drag_active = right_button_held_ && game_session_active_;
|
||||
// Roblox can keep Shift Lock active while chat has text focus. Honor that
|
||||
// native lock so camera motion stays relative and cannot leave the window.
|
||||
// Text editing only suppresses the host's fallback RMB camera capture.
|
||||
@@ -116,6 +116,10 @@ bool WindowPointerCaptureOwner::OnRightButton(bool pressed,
|
||||
return Pump(text_input_active);
|
||||
}
|
||||
|
||||
void WindowPointerCaptureOwner::SetGameSessionActive(bool active) {
|
||||
game_session_active_ = active;
|
||||
}
|
||||
|
||||
bool WindowPointerCaptureOwner::OnShiftKeyPressed(bool text_input_active) {
|
||||
focused_ = true;
|
||||
return Pump(text_input_active);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_WINDOW_WINDOW_POINTER_CAPTURE_OWNER_H_
|
||||
#define MOCKTAIL_WINDOW_WINDOW_POINTER_CAPTURE_OWNER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
@@ -31,6 +32,10 @@ class WindowPointerCaptureOwner final {
|
||||
// Android's lock-center query does not represent desktop RMB camera drag,
|
||||
// so the SDL owner tracks that transient capture source independently.
|
||||
bool OnRightButton(bool pressed, bool text_input_active);
|
||||
// The RMB fallback only stands in for a camera. Outside an experience there
|
||||
// is none, and capturing there freezes the cursor over Roblox's own UI.
|
||||
// Starts false: the app surface is what the client shows first.
|
||||
void SetGameSessionActive(bool active);
|
||||
// Shift can enable Roblox's persistent mouse lock while a transient RMB
|
||||
// camera drag is active. Refresh the native state after dispatching that
|
||||
// key so the subsequent RMB release preserves the intentional lock.
|
||||
@@ -62,6 +67,8 @@ class WindowPointerCaptureOwner final {
|
||||
bool clearing_ = false;
|
||||
bool focused_ = true;
|
||||
bool right_button_held_ = false;
|
||||
// Written by the experience composition, read on the SDL thread.
|
||||
std::atomic<bool> game_session_active_{false};
|
||||
bool native_lock_observed_ = false;
|
||||
bool native_lock_was_active_before_right_drag_ = false;
|
||||
bool shift_key_pressed_during_right_drag_ = false;
|
||||
|
||||
@@ -8,13 +8,15 @@ set -Eeuo pipefail
|
||||
readonly ROOT="${1:?source root is required}"
|
||||
readonly CMAKE_FILE="${ROOT}/CMakeLists.txt"
|
||||
readonly PKGBUILD="${ROOT}/packaging/arch/PKGBUILD"
|
||||
readonly AUR_STABLE_PKGBUILD="${ROOT}/packaging/aur/mocktail/PKGBUILD"
|
||||
readonly AUR_STABLE_SRCINFO="${ROOT}/packaging/aur/mocktail/.SRCINFO"
|
||||
readonly AUR_BIN_PKGBUILD="${ROOT}/packaging/aur/mocktail-bin/PKGBUILD"
|
||||
readonly AUR_BIN_SRCINFO="${ROOT}/packaging/aur/mocktail-bin/.SRCINFO"
|
||||
readonly AUR_PKGBUILD="${ROOT}/packaging/aur/mocktail-git/PKGBUILD"
|
||||
readonly AUR_SRCINFO="${ROOT}/packaging/aur/mocktail-git/.SRCINFO"
|
||||
readonly AUR_STABLE_PKGBUILD="${ROOT}/packaging/aur/nightcap/PKGBUILD"
|
||||
readonly AUR_STABLE_SRCINFO="${ROOT}/packaging/aur/nightcap/.SRCINFO"
|
||||
readonly AUR_BIN_PKGBUILD="${ROOT}/packaging/aur/nightcap-bin/PKGBUILD"
|
||||
readonly AUR_BIN_SRCINFO="${ROOT}/packaging/aur/nightcap-bin/.SRCINFO"
|
||||
readonly AUR_PKGBUILD="${ROOT}/packaging/aur/nightcap-git/PKGBUILD"
|
||||
readonly AUR_SRCINFO="${ROOT}/packaging/aur/nightcap-git/.SRCINFO"
|
||||
readonly AUR_WORKFLOW="${ROOT}/.github/workflows/aur.yml"
|
||||
readonly WORKFLOW="${ROOT}/.github/workflows/packages.yml"
|
||||
readonly RELEASE_WORKFLOW="${ROOT}/.github/workflows/release.yml"
|
||||
readonly README="${ROOT}/README.md"
|
||||
readonly STUB_CMAKE="${ROOT}/stubs/CMakeLists.txt"
|
||||
readonly ANDROID_STUB="${ROOT}/stubs/libandroid_stub.cc"
|
||||
@@ -35,31 +37,39 @@ grep -Fq 'CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON' "${CMAKE_FILE}" ||
|
||||
Fail 'DEB packages do not derive shared-library dependencies'
|
||||
grep -Fq 'CPACK_RPM_PACKAGE_AUTOREQPROV ON' "${CMAKE_FILE}" ||
|
||||
Fail 'RPM packages do not derive runtime requirements'
|
||||
grep -Fq 'pkgname=mocktail' "${PKGBUILD}" ||
|
||||
grep -Fq 'set(MOCKTAIL_PACKAGE_VERSION' "${CMAKE_FILE}" ||
|
||||
Fail 'native package version cannot be set from the release tag'
|
||||
grep -Fq 'https://github.com/CoderDayton/nightcap' "${CMAKE_FILE}" ||
|
||||
Fail 'native packages point at the upstream project page'
|
||||
grep -Fq 'pkgname=nightcap' "${PKGBUILD}" ||
|
||||
Fail 'Arch package name is not stable'
|
||||
grep -Fq "provides=('mocktail')" "${PKGBUILD}" ||
|
||||
Fail 'Arch package does not provide the upstream package name'
|
||||
grep -Fq "conflicts=('mocktail')" "${PKGBUILD}" ||
|
||||
Fail 'Arch package can be installed beside upstream Mocktail'
|
||||
grep -Fq "'sdl3>=3.4'" "${PKGBUILD}" ||
|
||||
Fail 'Arch package does not enforce the SDL minimum'
|
||||
grep -Fq "'libplacebo'" "${PKGBUILD}" ||
|
||||
Fail 'Arch package does not declare the graphics composition dependency'
|
||||
grep -Fq 'pkgname=mocktail-git' "${AUR_PKGBUILD}" ||
|
||||
grep -Fq 'pkgname=nightcap-git' "${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR VCS package name is not stable'
|
||||
grep -Fq 'pkgname=mocktail' "${AUR_STABLE_PKGBUILD}" ||
|
||||
grep -Fq 'pkgname=nightcap' "${AUR_STABLE_PKGBUILD}" ||
|
||||
Fail 'AUR source package name is not stable'
|
||||
grep -Fq '#tag=${pkgver}' "${AUR_STABLE_PKGBUILD}" ||
|
||||
grep -Fq '#tag=v${pkgver}' "${AUR_STABLE_PKGBUILD}" ||
|
||||
Fail 'AUR source package does not pin its Git tag'
|
||||
grep -Fq "'vulkan-headers'" "${AUR_STABLE_PKGBUILD}" ||
|
||||
Fail 'AUR source package does not use the packaged Vulkan headers'
|
||||
grep -Fq 'pkgbase = mocktail' "${AUR_STABLE_SRCINFO}" ||
|
||||
grep -Fq 'pkgbase = nightcap' "${AUR_STABLE_SRCINFO}" ||
|
||||
Fail 'AUR source package has no generated .SRCINFO metadata'
|
||||
grep -Fq 'pkgname=mocktail-bin' "${AUR_BIN_PKGBUILD}" ||
|
||||
grep -Fq 'pkgname=nightcap-bin' "${AUR_BIN_PKGBUILD}" ||
|
||||
Fail 'AUR binary package name is not stable'
|
||||
grep -Fq 'pkgbase = mocktail-bin' "${AUR_BIN_SRCINFO}" ||
|
||||
grep -Fq 'pkgbase = nightcap-bin' "${AUR_BIN_SRCINFO}" ||
|
||||
Fail 'AUR binary package has no generated .SRCINFO metadata'
|
||||
grep -Fq -- \
|
||||
"'mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main'" \
|
||||
"'nightcap::git+https://github.com/CoderDayton/nightcap.git#branch=main'" \
|
||||
"${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR package does not build from the upstream Git repository'
|
||||
grep -Fq "provides=('mocktail')" "${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR package does not build from this project Git repository'
|
||||
grep -Fq "provides=('mocktail' 'nightcap')" "${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR VCS package does not provide the stable package name'
|
||||
grep -Fq "'vulkan-headers'" "${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR VCS package does not use the packaged Vulkan headers'
|
||||
@@ -76,8 +86,26 @@ grep -Fq -- \
|
||||
'-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=/usr/share/mocktail/metadata/' \
|
||||
"${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR package embeds a build-tree compatibility manifest path'
|
||||
grep -Fq 'pkgbase = mocktail-git' "${AUR_SRCINFO}" ||
|
||||
grep -Fq 'pkgbase = nightcap-git' "${AUR_SRCINFO}" ||
|
||||
Fail 'AUR package has no generated .SRCINFO metadata'
|
||||
grep -Fq 'git describe' "${AUR_PKGBUILD}" ||
|
||||
Fail 'AUR VCS package does not version from the release tag'
|
||||
grep -Fq 'git describe' "${AUR_WORKFLOW}" ||
|
||||
Fail 'AUR workflow does not version from the release tag'
|
||||
for pkgbuild in "${PKGBUILD}" "${AUR_STABLE_PKGBUILD}" "${AUR_BIN_PKGBUILD}"; do
|
||||
grep -Fqx 'pkgver=0.3.1' "${pkgbuild}" ||
|
||||
Fail "package version does not match the release being prepared: ${pkgbuild}"
|
||||
done
|
||||
|
||||
# Nightcap keeps its AUR recipes in the tree. Nothing publishes them.
|
||||
for unexpected in \
|
||||
'aur.archlinux.org' \
|
||||
'AUR_SSH_PRIVATE_KEY' \
|
||||
'push origin'; do
|
||||
if grep -Fq -- "${unexpected}" "${AUR_WORKFLOW}"; then
|
||||
Fail "AUR workflow should not publish: ${unexpected}"
|
||||
fi
|
||||
done
|
||||
grep -Fq 'LINKER:--no-as-needed' "${STUB_CMAKE}" ||
|
||||
Fail 'system shims can lose their host libc dependencies'
|
||||
grep -Fq 'MOCKTAIL_MINIZIP_HAS_STREAM_TELL' "${STUB_CMAKE}" ||
|
||||
@@ -116,6 +144,29 @@ for unexpected in \
|
||||
fi
|
||||
done
|
||||
|
||||
for expected in \
|
||||
'-G DEB' \
|
||||
'-G RPM' \
|
||||
'makepkg --dir' \
|
||||
'-DMOCKTAIL_PACKAGE_NAME=nightcap' \
|
||||
'-DMOCKTAIL_PACKAGE_VERSION=' \
|
||||
'Nightcap-x86_64.AppImage' \
|
||||
'gh release upload'; do
|
||||
grep -Fq -- "${expected}" "${RELEASE_WORKFLOW}" ||
|
||||
Fail "release workflow is missing: ${expected}"
|
||||
done
|
||||
|
||||
# Nightcap publishes its packages on the release page only. Pushing them to
|
||||
# APT, RPM or AUR repositories is out of scope for this workflow.
|
||||
for unexpected in \
|
||||
'createrepo' \
|
||||
'reprepro' \
|
||||
'aur.archlinux.org'; do
|
||||
if grep -Fq -- "${unexpected}" "${RELEASE_WORKFLOW}"; then
|
||||
Fail "release workflow should not publish to a repository: ${unexpected}"
|
||||
fi
|
||||
done
|
||||
|
||||
grep -Fq 'Ubuntu 26.04+' "${README}" ||
|
||||
Fail 'README has no Ubuntu source dependency guide'
|
||||
grep -Fq 'Arch Linux' "${README}" ||
|
||||
|
||||
@@ -277,6 +277,67 @@ TEST_F(RobloxInputRouterTest, HoldsPointerPositionDuringRelativeMotion) {
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[3].y, 100.0f);
|
||||
}
|
||||
|
||||
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeMotionCarriesAPosition) {
|
||||
// Wayland reports a drifting absolute position during relative mode instead
|
||||
// of zeroes, and it can leave the window. The capture flag decides, not the
|
||||
// coordinates, so the held position survives the drift and the reversal.
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseMotionEvent{
|
||||
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
||||
.dispatched());
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseMotionEvent{
|
||||
1840.0f, 470.0f, 1200.0f, 110.0f, 0, true}))
|
||||
.dispatched());
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseMotionEvent{
|
||||
-510.0f, 430.0f, -2350.0f, -40.0f, 0, true}))
|
||||
.dispatched());
|
||||
|
||||
ASSERT_EQ(probe_.mouse_moves.size(), 3U);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 640.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].y, 360.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].delta_x, 1200.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].x, 640.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].y, 360.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].delta_x, -2350.0f);
|
||||
}
|
||||
|
||||
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeButtonCarriesAPosition) {
|
||||
// The release that ends a Wayland camera drag carries the drifted position
|
||||
// too. It must not move the pointer Roblox draws.
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseMotionEvent{
|
||||
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
||||
.dispatched());
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseButtonEvent{
|
||||
false, SDL_BUTTON_RIGHT, 1, 1840.0f, 470.0f, true}))
|
||||
.dispatched());
|
||||
|
||||
ASSERT_FALSE(probe_.mouse_buttons.empty());
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().x, 640.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_buttons.back().y, 360.0f);
|
||||
}
|
||||
|
||||
TEST_F(RobloxInputRouterTest, HoldsPointerWhenRelativeWheelCarriesAPosition) {
|
||||
// Zooming the camera mid-drag scrolls while capture is held. Wayland stamps
|
||||
// the drifted position on the wheel event too, so the capture flag decides
|
||||
// there as well.
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseMotionEvent{
|
||||
640.0f, 360.0f, 0.0f, 0.0f, 0, false}))
|
||||
.dispatched());
|
||||
ASSERT_TRUE(router_
|
||||
.HandleEvent(Event(platform::MouseWheelEvent{
|
||||
0.0f, -1.0f, 1840.0f, 470.0f, true}))
|
||||
.dispatched());
|
||||
|
||||
ASSERT_FALSE(probe_.mouse_wheels.empty());
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().x, 640.0f);
|
||||
EXPECT_FLOAT_EQ(probe_.mouse_wheels.back().y, 360.0f);
|
||||
}
|
||||
|
||||
TEST_F(RobloxInputRouterTest,
|
||||
AnchorsUnknownPointerAtViewportCenterForRelativeMotion) {
|
||||
// No absolute position has been seen yet, so the held position is the
|
||||
|
||||
@@ -569,6 +569,85 @@ TEST(RobloxTextDisplayStateTest, WrapsMultilineTextInsideNativeTextBox) {
|
||||
EXPECT_TRUE(overlay.Shutdown().ok());
|
||||
}
|
||||
|
||||
TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) {
|
||||
RobloxTextSurfaceOverlay overlay;
|
||||
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
||||
RobloxTextDisplaySink sink = overlay.sink();
|
||||
const auto rightmost_pixel = [&](const std::string& text,
|
||||
RobloxTextDisplayEvent event) {
|
||||
RobloxTextDisplayUpdate update =
|
||||
Show(1, text, static_cast<int32_t>(text.size()));
|
||||
update.event = event;
|
||||
update.font_size = 18.0F;
|
||||
sink.update(sink.context, update);
|
||||
MocktailTextOverlayFrameInfo frame;
|
||||
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
||||
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
||||
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
||||
return FindAlphaBounds(frame, rgba).maximum_x;
|
||||
};
|
||||
|
||||
const int typed = rightmost_pixel("hi", RobloxTextDisplayEvent::kShow);
|
||||
const int spaced = rightmost_pixel("hi ", RobloxTextDisplayEvent::kUpdate);
|
||||
|
||||
EXPECT_GT(spaced, typed);
|
||||
EXPECT_TRUE(overlay.Shutdown().ok());
|
||||
}
|
||||
|
||||
TEST(RobloxTextDisplayStateTest, KeepsCaretBeforeTrailingSpaceDistinct) {
|
||||
RobloxTextSurfaceOverlay overlay;
|
||||
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
||||
RobloxTextDisplaySink sink = overlay.sink();
|
||||
const std::string text = "hi ";
|
||||
const auto rightmost_pixel = [&](int32_t cursor_utf16,
|
||||
RobloxTextDisplayEvent event) {
|
||||
RobloxTextDisplayUpdate update = Show(1, text, cursor_utf16);
|
||||
update.event = event;
|
||||
update.font_size = 18.0F;
|
||||
sink.update(sink.context, update);
|
||||
MocktailTextOverlayFrameInfo frame;
|
||||
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
||||
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
||||
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
||||
const auto bounds = FindAlphaBounds(frame, rgba);
|
||||
EXPECT_TRUE(bounds.valid());
|
||||
return bounds.valid() ? bounds.maximum_x : -1;
|
||||
};
|
||||
|
||||
const int before_space = rightmost_pixel(2, RobloxTextDisplayEvent::kShow);
|
||||
const int after_space = rightmost_pixel(3, RobloxTextDisplayEvent::kUpdate);
|
||||
|
||||
EXPECT_GT(after_space, before_space);
|
||||
EXPECT_TRUE(overlay.Shutdown().ok());
|
||||
}
|
||||
|
||||
TEST(RobloxTextDisplayStateTest, ReopensFontsAfterPointSizeChange) {
|
||||
RobloxTextSurfaceOverlay overlay;
|
||||
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
||||
RobloxTextDisplaySink sink = overlay.sink();
|
||||
const std::string text = "size";
|
||||
const auto text_width = [&](float font_size, RobloxTextDisplayEvent event) {
|
||||
RobloxTextDisplayUpdate update = Show(1, text, 4);
|
||||
update.event = event;
|
||||
update.area_height = 64;
|
||||
update.font_size = font_size;
|
||||
sink.update(sink.context, update);
|
||||
MocktailTextOverlayFrameInfo frame;
|
||||
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
||||
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
||||
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
||||
const auto bounds = FindAlphaBounds(frame, rgba);
|
||||
EXPECT_TRUE(bounds.valid());
|
||||
return bounds.valid() ? bounds.maximum_x - bounds.minimum_x : 0;
|
||||
};
|
||||
|
||||
const int small = text_width(14.0F, RobloxTextDisplayEvent::kShow);
|
||||
const int large = text_width(28.0F, RobloxTextDisplayEvent::kUpdate);
|
||||
|
||||
EXPECT_GT(large, small);
|
||||
EXPECT_TRUE(overlay.Shutdown().ok());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
namespace {
|
||||
@@ -206,10 +208,67 @@ VKAPI_ATTR void VKAPI_CALL FakeFreeMemory(VkDevice, VkDeviceMemory,
|
||||
const VkAllocationCallbacks*) {
|
||||
++g_free_memory_calls;
|
||||
}
|
||||
// Records the host-write barrier that must precede each staged copy, so the
|
||||
// decode's writes are visible to the device even though they land after the
|
||||
// submit that reads them.
|
||||
std::uint32_t g_barriers_before_copy = 0;
|
||||
VkPipelineStageFlags g_barrier_src_stage = 0;
|
||||
VkPipelineStageFlags g_barrier_dst_stage = 0;
|
||||
VkAccessFlags g_barrier_src_access = 0;
|
||||
VkAccessFlags g_barrier_dst_access = 0;
|
||||
std::uint32_t g_pending_barriers = 0;
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FakePipelineBarrier(
|
||||
VkCommandBuffer, VkPipelineStageFlags src_stage,
|
||||
VkPipelineStageFlags dst_stage, VkDependencyFlags, std::uint32_t count,
|
||||
const VkMemoryBarrier* barriers, std::uint32_t, const VkBufferMemoryBarrier*,
|
||||
std::uint32_t, const VkImageMemoryBarrier*) {
|
||||
g_barrier_src_stage = src_stage;
|
||||
g_barrier_dst_stage = dst_stage;
|
||||
if (count > 0) {
|
||||
g_barrier_src_access = barriers[0].srcAccessMask;
|
||||
g_barrier_dst_access = barriers[0].dstAccessMask;
|
||||
}
|
||||
++g_pending_barriers;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FakeCopyBufferToImage(
|
||||
VkCommandBuffer, VkBuffer, VkImage, VkImageLayout, std::uint32_t count,
|
||||
const VkBufferImageCopy* regions) {
|
||||
g_copied_extent = count > 0 ? regions[0].imageExtent : VkExtent3D{};
|
||||
g_barriers_before_copy = g_pending_barriers;
|
||||
}
|
||||
|
||||
constexpr std::uintptr_t kTimelineSemaphore = 0x900;
|
||||
|
||||
std::mutex g_signal_mutex;
|
||||
std::vector<std::uint64_t> g_signalled;
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FakeCreateSemaphore(
|
||||
VkDevice, const VkSemaphoreCreateInfo*, const VkAllocationCallbacks*,
|
||||
VkSemaphore* semaphore) {
|
||||
*semaphore = FakeHandle<VkSemaphore>(kTimelineSemaphore);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL
|
||||
FakeSignalSemaphore(VkDevice, const VkSemaphoreSignalInfo* info) {
|
||||
std::lock_guard<std::mutex> lock(g_signal_mutex);
|
||||
g_signalled.push_back(info->value);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FakeDestroySemaphore(VkDevice, VkSemaphore,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
std::vector<std::uint64_t> SignalledValues() {
|
||||
std::lock_guard<std::mutex> lock(g_signal_mutex);
|
||||
return g_signalled;
|
||||
}
|
||||
|
||||
void ResetSignalledValues() {
|
||||
std::lock_guard<std::mutex> lock(g_signal_mutex);
|
||||
g_signalled.clear();
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
||||
@@ -217,6 +276,12 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
||||
const auto is = [name](const char* candidate) {
|
||||
return std::strcmp(name, candidate) == 0;
|
||||
};
|
||||
if (is("vkCreateSemaphore"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateSemaphore);
|
||||
if (is("vkSignalSemaphore"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeSignalSemaphore);
|
||||
if (is("vkDestroySemaphore"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeDestroySemaphore);
|
||||
if (is("vkCreateImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateImage);
|
||||
if (is("vkCreateBuffer")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCreateBuffer);
|
||||
if (is("vkGetBufferMemoryRequirements"))
|
||||
@@ -229,6 +294,8 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL FakeGetDeviceProcAddr(
|
||||
if (is("vkFreeMemory")) return reinterpret_cast<PFN_vkVoidFunction>(FakeFreeMemory);
|
||||
if (is("vkCmdCopyBufferToImage"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakeCopyBufferToImage);
|
||||
if (is("vkCmdPipelineBarrier"))
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(FakePipelineBarrier);
|
||||
if (is("vkCmdCopyImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeCopyImage);
|
||||
if (is("vkCmdBlitImage")) return reinterpret_cast<PFN_vkVoidFunction>(FakeBlitImage);
|
||||
return nullptr;
|
||||
@@ -805,5 +872,213 @@ TEST(VulkanEtc2EmulationTest, ReadsCompressedRowsAtTheRequestedStride) {
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
// Records one 4x4 ETC2 upload whose every texel decodes to 134, and returns
|
||||
// the image it targets.
|
||||
VkImage RecordDecodeOf134(VulkanEtc2Emulation* emulation, VkDevice device,
|
||||
VkBuffer source, VkDeviceMemory source_memory,
|
||||
VkCommandBuffer command_buffer) {
|
||||
VkImageCreateInfo image_info{};
|
||||
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
image_info.format = VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK;
|
||||
image_info.extent = {4, 4, 1};
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
EXPECT_EQ(emulation->CreateImage(device, &image_info, nullptr, &image),
|
||||
VK_SUCCESS);
|
||||
EXPECT_EQ(emulation->BindBufferMemory(device, source, source_memory, 0),
|
||||
VK_SUCCESS);
|
||||
void* mapped = nullptr;
|
||||
EXPECT_EQ(emulation->MapMemory(device, source_memory, 0, g_source.size(), 0,
|
||||
&mapped),
|
||||
VK_SUCCESS);
|
||||
|
||||
const std::array<std::uint8_t, 8> block = {0x81, 0x81, 0x81, 0x02,
|
||||
0, 0, 0, 0};
|
||||
std::memcpy(g_source.data(), block.data(), block.size());
|
||||
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageExtent = {4, 4, 1};
|
||||
emulation->CmdCopyBufferToImage(device, command_buffer, source, image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
|
||||
®ion);
|
||||
return image;
|
||||
}
|
||||
|
||||
VkPhysicalDeviceMemoryProperties HostVisibleMemory() {
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
memory.memoryTypeCount = 1;
|
||||
memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
return memory;
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, DecodesOffTheCallingThreadWithATimeline) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x18);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x208);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x708);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
ResetSignalledValues();
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x28), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, true);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer);
|
||||
|
||||
const Etc2SubmitWait wait =
|
||||
emulation.PrepareSubmit(&command_buffer, 1, true);
|
||||
EXPECT_EQ(wait.semaphore, FakeHandle<VkSemaphore>(kTimelineSemaphore));
|
||||
EXPECT_GT(wait.value, 0U);
|
||||
|
||||
// Releasing the command buffer waits for the decode that writes its staging.
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
EXPECT_EQ(g_staging[0], 134);
|
||||
EXPECT_EQ(SignalledValues(), std::vector<std::uint64_t>{wait.value});
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, DecodesOnTheCallerWithoutATimeline) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x19);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x209);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x709);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
ResetSignalledValues();
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x29), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, false);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer);
|
||||
|
||||
const Etc2SubmitWait wait =
|
||||
emulation.PrepareSubmit(&command_buffer, 1, true);
|
||||
EXPECT_EQ(wait.semaphore, VK_NULL_HANDLE);
|
||||
EXPECT_EQ(wait.value, 0U);
|
||||
// Decoded before the call returned, so no wait is owed.
|
||||
EXPECT_EQ(g_staging[0], 134);
|
||||
EXPECT_TRUE(SignalledValues().empty());
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, UnmapWaitsForDecodesReadingThatMemory) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x1C);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x20C);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x70D);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
ResetSignalledValues();
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x2C), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, true);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer);
|
||||
|
||||
const Etc2SubmitWait wait =
|
||||
emulation.PrepareSubmit(&command_buffer, 1, true);
|
||||
ASSERT_NE(wait.semaphore, VK_NULL_HANDLE);
|
||||
|
||||
// A deferred gather reads the application's mapping on a worker, so the
|
||||
// unmap must not return while a decode can still dereference it.
|
||||
emulation.UnmapMemory(device, source_memory);
|
||||
const std::vector<std::uint64_t> signalled = SignalledValues();
|
||||
ASSERT_FALSE(signalled.empty()) << "unmap returned with a decode in flight";
|
||||
EXPECT_GE(signalled.back(), wait.value);
|
||||
EXPECT_EQ(g_staging[0], 134);
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, FreeMemoryWaitsForDecodesReadingThatMemory) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x1D);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x20D);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x70E);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
ResetSignalledValues();
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x2D), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, true);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer);
|
||||
|
||||
const Etc2SubmitWait wait =
|
||||
emulation.PrepareSubmit(&command_buffer, 1, true);
|
||||
ASSERT_NE(wait.semaphore, VK_NULL_HANDLE);
|
||||
|
||||
emulation.FreeMemory(device, source_memory, nullptr);
|
||||
const std::vector<std::uint64_t> signalled = SignalledValues();
|
||||
ASSERT_FALSE(signalled.empty()) << "free returned with a decode in flight";
|
||||
EXPECT_GE(signalled.back(), wait.value);
|
||||
EXPECT_EQ(g_staging[0], 134);
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, GuardsStagedCopiesWithAHostWriteBarrier) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x1B);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x20B);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer command_buffer = FakeHandle<VkCommandBuffer>(0x70C);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
g_pending_barriers = 0;
|
||||
g_barriers_before_copy = 0;
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x2B), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, true);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, command_buffer);
|
||||
|
||||
// An asynchronous decode writes the staging after the submit that reads it,
|
||||
// so the queue submission's host-to-device domain operation does not cover
|
||||
// those writes; the copy must be preceded by an explicit barrier.
|
||||
EXPECT_EQ(g_barriers_before_copy, 1U);
|
||||
EXPECT_EQ(g_barrier_src_stage,
|
||||
static_cast<VkPipelineStageFlags>(VK_PIPELINE_STAGE_HOST_BIT));
|
||||
EXPECT_NE(g_barrier_dst_stage &
|
||||
static_cast<VkPipelineStageFlags>(VK_PIPELINE_STAGE_TRANSFER_BIT),
|
||||
0U);
|
||||
EXPECT_EQ(g_barrier_src_access,
|
||||
static_cast<VkAccessFlags>(VK_ACCESS_HOST_WRITE_BIT));
|
||||
EXPECT_NE(g_barrier_dst_access &
|
||||
static_cast<VkAccessFlags>(VK_ACCESS_TRANSFER_READ_BIT),
|
||||
0U);
|
||||
emulation.PrepareSubmit(&command_buffer, 1, true);
|
||||
emulation.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
TEST(VulkanEtc2EmulationTest, SignalsDecodeValuesInHandOutOrder) {
|
||||
const VkDevice device = FakeHandle<VkDevice>(0x1A);
|
||||
const VkBuffer source = FakeHandle<VkBuffer>(0x20A);
|
||||
const VkDeviceMemory source_memory = FakeHandle<VkDeviceMemory>(kSourceMemory);
|
||||
const VkCommandBuffer first = FakeHandle<VkCommandBuffer>(0x70A);
|
||||
const VkCommandBuffer second = FakeHandle<VkCommandBuffer>(0x70B);
|
||||
g_source.fill(0);
|
||||
g_staging.fill(0xAA);
|
||||
ResetSignalledValues();
|
||||
|
||||
VulkanEtc2Emulation emulation;
|
||||
emulation.RegisterDevice(device, FakeHandle<VkPhysicalDevice>(0x2A), true,
|
||||
HostVisibleMemory(), FakeGetDeviceProcAddr, true);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, first);
|
||||
RecordDecodeOf134(&emulation, device, source, source_memory, second);
|
||||
|
||||
const Etc2SubmitWait one = emulation.PrepareSubmit(&first, 1, true);
|
||||
const Etc2SubmitWait two = emulation.PrepareSubmit(&second, 1, true);
|
||||
EXPECT_GT(two.value, one.value);
|
||||
|
||||
emulation.ReleaseCommandBuffer(first);
|
||||
emulation.ReleaseCommandBuffer(second);
|
||||
|
||||
const std::vector<std::uint64_t> signalled = SignalledValues();
|
||||
ASSERT_FALSE(signalled.empty());
|
||||
EXPECT_EQ(signalled.back(), two.value);
|
||||
for (std::size_t index = 1; index < signalled.size(); ++index) {
|
||||
EXPECT_GT(signalled[index], signalled[index - 1]) << "at " << index;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::graphics
|
||||
@@ -135,11 +135,44 @@ TEST(WindowPointerCaptureOwnerTest, QueryCanClearItselfWithoutDeadlock) {
|
||||
EXPECT_TRUE(owner.RegisterQuery(Query, &replacement));
|
||||
}
|
||||
|
||||
TEST(WindowPointerCaptureOwnerTest, RightDragDoesNotCaptureOutsideAnExperience) {
|
||||
// The app surface has no camera. Capturing there would freeze the cursor
|
||||
// over Roblox's own menu UI.
|
||||
FakeBackend backend;
|
||||
QueryState query{true, false};
|
||||
WindowPointerCaptureOwner owner(&backend);
|
||||
ASSERT_TRUE(owner.RegisterQuery(Query, &query));
|
||||
ASSERT_TRUE(owner.Pump(false));
|
||||
|
||||
EXPECT_TRUE(owner.OnRightButton(true, false));
|
||||
EXPECT_FALSE(owner.captured());
|
||||
EXPECT_TRUE(owner.OnRightButton(false, false));
|
||||
EXPECT_FALSE(owner.captured());
|
||||
}
|
||||
|
||||
TEST(WindowPointerCaptureOwnerTest, RightDragCapturesOnceTheGameSurfaceIsLive) {
|
||||
FakeBackend backend;
|
||||
QueryState query{true, false};
|
||||
WindowPointerCaptureOwner owner(&backend);
|
||||
ASSERT_TRUE(owner.RegisterQuery(Query, &query));
|
||||
ASSERT_TRUE(owner.Pump(false));
|
||||
|
||||
owner.SetGameSessionActive(true);
|
||||
EXPECT_TRUE(owner.OnRightButton(true, false));
|
||||
EXPECT_TRUE(owner.captured());
|
||||
|
||||
// Leaving the experience releases a capture that is still held.
|
||||
owner.SetGameSessionActive(false);
|
||||
EXPECT_TRUE(owner.Pump(false));
|
||||
EXPECT_FALSE(owner.captured());
|
||||
}
|
||||
|
||||
TEST(WindowPointerCaptureOwnerTest, RightDragCapturesUntilButtonRelease) {
|
||||
FakeBackend backend;
|
||||
QueryState query{true, false};
|
||||
WindowPointerCaptureOwner owner(&backend);
|
||||
ASSERT_TRUE(owner.RegisterQuery(Query, &query));
|
||||
owner.SetGameSessionActive(true);
|
||||
ASSERT_TRUE(owner.Pump(false));
|
||||
|
||||
EXPECT_TRUE(owner.OnRightButton(true, false));
|
||||
@@ -230,6 +263,7 @@ TEST(WindowPointerCaptureOwnerTest, RightDragDoesNotDependOnNativeLockQuery) {
|
||||
QueryState query{false, false};
|
||||
WindowPointerCaptureOwner owner(&backend);
|
||||
ASSERT_TRUE(owner.RegisterQuery(Query, &query));
|
||||
owner.SetGameSessionActive(true);
|
||||
|
||||
EXPECT_TRUE(owner.OnRightButton(true, false));
|
||||
EXPECT_TRUE(owner.captured());
|
||||
@@ -244,6 +278,7 @@ TEST(WindowPointerCaptureOwnerTest, TextAndFocusCancelRightDragCapture) {
|
||||
QueryState query{true, false};
|
||||
WindowPointerCaptureOwner owner(&backend);
|
||||
ASSERT_TRUE(owner.RegisterQuery(Query, &query));
|
||||
owner.SetGameSessionActive(true);
|
||||
|
||||
EXPECT_TRUE(owner.OnRightButton(true, true));
|
||||
EXPECT_FALSE(owner.captured());
|
||||
|
||||
Reference in new issue
Block a user