mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 21:00:47 +02:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f6847d6d7 | ||
|
|
30d50f15d1 | ||
|
|
ab940acfb5 | ||
|
|
719b1fdd22 | ||
|
|
1fa2e116b3 | ||
|
|
40237fae72 | ||
|
|
854969457f | ||
|
|
b54c896498 | ||
|
|
1cd7b7db8f | ||
|
|
cdbf9ac1eb | ||
|
|
f383fbbff4 |
No files matched your search
@@ -8,22 +8,32 @@ on:
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -64,3 +74,44 @@ jobs:
|
||||
bash tests/managed_payload_build_contract_test.sh .
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
bash tests/anylinux_dependencies_test.sh .
|
||||
|
||||
native-build:
|
||||
name: Native build (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-26.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-26.04-arm
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
build-essential binutils cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev libpng-dev zlib1g-dev
|
||||
|
||||
- name: Build and check native executables
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = "${{ matrix.arch }}"
|
||||
cmake -S . -B build-ci -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF
|
||||
cmake --build build-ci --parallel 4
|
||||
./build-ci/mocktail --help
|
||||
./build-ci/mocktail_updater status
|
||||
+127
-67
@@ -38,15 +38,24 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
name: Flatpak build (${{ matrix.arch }})
|
||||
if: >-
|
||||
github.event_name != 'workflow_run' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main')
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
env:
|
||||
FLATPAK_ARCH: ${{ matrix.arch }}
|
||||
MOCKTAIL_PUBLISH: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == 'main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
MOCKTAIL_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
steps:
|
||||
@@ -57,8 +66,92 @@ jobs:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch="$FLATPAK_ARCH" \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
--disable-rofiles-fuse \
|
||||
--force-clean \
|
||||
--default-branch=stable \
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
flatpak build-bundle \
|
||||
--arch="$FLATPAK_ARCH" \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
"Mocktail-${FLATPAK_ARCH}.flatpak" \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-${{ matrix.arch }}-flatpak
|
||||
path: Mocktail-${{ matrix.arch }}.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Assemble multi-architecture Flatpak repository
|
||||
if: >-
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main') ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Download both Flatpak bundles
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Mocktail-*-flatpak
|
||||
path: flatpak-bundles
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download package repositories
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -84,86 +177,53 @@ jobs:
|
||||
-d native-repositories/downloads && \
|
||||
-s native-repositories/mocktail-packages.gpg ]]
|
||||
|
||||
- name: Install Flatpak builder
|
||||
- name: Install Flatpak repository tools
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
sudo apt-get install --yes --no-install-recommends flatpak gnupg ostree
|
||||
|
||||
- name: Build Flatpak repository and bundle
|
||||
- name: Sign and publish both architectures
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
--disable-rofiles-fuse \
|
||||
--force-clean \
|
||||
--default-branch=stable \
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && -n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
[[ -s flatpak-bundles/Mocktail-x86_64.flatpak && \
|
||||
-s flatpak-bundles/Mocktail-aarch64.flatpak ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
ostree --repo=repo init --mode=archive
|
||||
for arch in x86_64 aarch64; do
|
||||
flatpak build-import-bundle \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
|
||||
public native-repositories
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-x86_64-flatpak
|
||||
path: Mocktail-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
--no-update-summary \
|
||||
repo "flatpak-bundles/Mocktail-${arch}.flatpak"
|
||||
ostree --repo=repo refs | \
|
||||
grep -Fxq "app/space.bigrat.mocktail/${arch}/stable"
|
||||
done
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo flatpak-bundles/Mocktail-x86_64.flatpak \
|
||||
flatpak-bundles/Mocktail-aarch64.flatpak \
|
||||
mocktail-flatpak.gpg public native-repositories
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
@@ -177,7 +237,7 @@ jobs:
|
||||
github.event.workflow_run.head_branch == 'main') ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main')
|
||||
needs: flatpak
|
||||
needs: publish
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
@@ -45,16 +45,19 @@ jobs:
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
vulkan-headers zlib libplacebo libpng bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
# Reduced ABI-compatible packages belong only in this builder.
|
||||
@@ -140,6 +143,117 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
appimage-aarch64:
|
||||
name: AppImage (AnyLinux aarch64)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build aarch64 AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm --platform linux/arm64 \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/pkgforge-dev/archlinux:latest@sha256:b2507f1964270cab3cc190aa8df858521f6a7e45e969146a012877891d5dfc9b bash -Eeuo pipefail -c '
|
||||
test "$(uname -m)" = aarch64
|
||||
pacman-key --init
|
||||
pacman -Syy --noconfirm archlinux-keyring
|
||||
pacman -Syu --noconfirm --needed \
|
||||
base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo libpng bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-aarch64
|
||||
echo "3b64db0727a9d5c8212c4b2342770583ce552deac0644d2d6033e9ec280f7635 /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-aarch64-linux
|
||||
echo "298f5293c41f9b2b46290b246e6419783940824ce20b98775a5a153b18fe8422 /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-aarch64
|
||||
echo "5686f126b22d944b2b9c862ee004b4c590b5a5e5215d8790cc0420f1fbbea942 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-aarch64
|
||||
echo "2d513c43ad652163e5e31c1a96f3c736e7e188e661ff62fc9143ea7147bd936b /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage-aarch64 JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-aarch64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-aarch64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test aarch64 AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
timeout 60s dist/Mocktail-aarch64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-aarch64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload aarch64 AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage-aarch64
|
||||
path: dist/Mocktail-aarch64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -169,7 +283,7 @@ jobs:
|
||||
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
|
||||
libvulkan-dev libplacebo-dev libpng-dev zlib1g-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
@@ -217,7 +331,7 @@ jobs:
|
||||
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
|
||||
libplacebo-devel libpng-devel zlib-ng-compat-devel
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
@@ -261,7 +375,7 @@ jobs:
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig \
|
||||
libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
libplacebo libpng vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
@@ -283,7 +397,7 @@ jobs:
|
||||
publish:
|
||||
name: Publish continuous packages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
needs: [appimage, appimage-aarch64, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
@@ -305,7 +419,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
mv -- release/*.AppImage release/mocktail-nightly.AppImage
|
||||
mv -- release/Mocktail-x86_64.AppImage release/mocktail-nightly.AppImage
|
||||
mv -- release/Mocktail-aarch64.AppImage release/mocktail-nightly-aarch64.AppImage
|
||||
mv -- release/*.deb release/mocktail-nightly.deb
|
||||
mv -- release/*.rpm release/mocktail-nightly.rpm
|
||||
mv -- release/*.pkg.tar.zst release/mocktail-nightly.pkg.tar.zst
|
||||
@@ -360,7 +475,7 @@ jobs:
|
||||
gh release upload continuous release/* --clobber
|
||||
while IFS= read -r asset; do
|
||||
case "$asset" in
|
||||
mocktail-nightly.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
|
||||
mocktail-nightly.AppImage|mocktail-nightly-aarch64.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
|
||||
;;
|
||||
Mocktail-*.AppImage|mocktail_*.deb|mocktail-*.deb|mocktail-*.rpm|mocktail-*.pkg.tar.zst)
|
||||
gh release delete-asset continuous "$asset" --yes
|
||||
|
||||
+8
-2
@@ -9,7 +9,7 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 1.0.4
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
DESCRIPTION "Android Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
@@ -264,6 +264,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/command_line.cc
|
||||
src/runtime/crash_report_policy.cc
|
||||
src/runtime/discord_rpc.cc
|
||||
src/runtime/device_memory_profile.cc
|
||||
src/runtime/device_profile.cc
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
@@ -647,6 +648,11 @@ target_link_options(mocktail PRIVATE
|
||||
)
|
||||
|
||||
# Native FreeBSD helper for Linuxulator's unsupported DF socket option.
|
||||
# The helper is x86-64 assembly; other host architectures have nothing to
|
||||
# assemble.
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
|
||||
set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF)
|
||||
endif()
|
||||
option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER
|
||||
"Build the native FreeBSD Linuxulator socket helper" ON)
|
||||
if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER)
|
||||
@@ -791,7 +797,7 @@ set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION
|
||||
"Runs the Android x86-64 Roblox client on Linux."
|
||||
"Runs the Android Roblox client on Linux."
|
||||
)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# FAQ
|
||||
|
||||
## How do I enable Discord RPC?
|
||||
|
||||
RPC is disabled by default. Launch Mocktail once to create the config, then
|
||||
close it. Open `config.yaml`:
|
||||
|
||||
- Native / AppImage: `~/.config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/config/mocktail/config.yaml`
|
||||
|
||||
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
|
||||
|
||||
Set `enabled` to `true` under `integrations.discord_rpc`. If the block is
|
||||
missing, add it under the existing `integrations` section:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
discord_rpc:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Save the file, open Discord Desktop, and restart Mocktail. Set `enabled`
|
||||
back to `false` to disable RPC.
|
||||
|
||||
## Where can I find logs for a bug report?
|
||||
|
||||
Mocktail saves logs automatically. Reproduce the issue, close Mocktail, and
|
||||
attach `latest.log`:
|
||||
|
||||
- Native / AppImage: `~/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
|
||||
|
||||
## Can I run multiple Roblox instances?
|
||||
|
||||
No. Mocktail won't add multi-instance launching. It would mean working around
|
||||
Roblox's restrictions on running several clients. The [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use)
|
||||
prohibit bypassing technical protections, and violating the terms can get your
|
||||
account suspended.
|
||||
|
||||
Bloxstrap brought the option back in [v2.9.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.9.0)
|
||||
with a "use at your own risk" warning, then removed it in
|
||||
[v2.10.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.10.0)
|
||||
after Roblox added measures against running multiple clients.
|
||||
|
||||
## Can I play in VR?
|
||||
|
||||
VR is experimental. Install the [build dependencies](README.md#building),
|
||||
then switch to the `vr` branch and build:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git switch vr
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
```
|
||||
|
||||
Start WiVRn or SteamVR/ALVR, connect your headset, then run:
|
||||
|
||||
```bash
|
||||
./build/mocktail -vr
|
||||
```
|
||||
@@ -129,8 +129,8 @@ auto-run: ## Build/run loop with crash summaries
|
||||
run-gles: build ## Run strict system EGL/OpenGL ES 3 without Vulkan fallback
|
||||
@$(BINARY) --graphics opengl
|
||||
|
||||
run-angle: ## Run with ANGLE/Vulkan compatibility backend
|
||||
@MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan ./scripts/run_sober.sh
|
||||
run-angle: build ## Run with ANGLE/Vulkan compatibility backend
|
||||
@$(BINARY) --graphics angle-vulkan
|
||||
|
||||
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
|
||||
@./scripts/update_roblox_payload.sh
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/rhgpfcmFSD)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
|
||||
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
|
||||
the Linux side.
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux and has experimental
|
||||
`aarch64` builds for the Android `arm64-v8a` client. It also runs in a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. Mocktail provides the Android ABI
|
||||
and JNI pieces the client expects, then connects them to SDL3 and Vulkan or
|
||||
OpenGL on the Linux side.
|
||||
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
@@ -124,10 +125,12 @@ To see the most Frequently Asked Question go check [FAQ.md](FAQ.md)
|
||||
|
||||
## Building
|
||||
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail 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,
|
||||
Linux `x86_64` is supported. Linux `aarch64` builds are available, with Roblox
|
||||
runtime validation still experimental. FreeBSD 15.1 Linuxulator support is
|
||||
experimental and has been tested with an `x86_64` Fedora 44 userspace. On
|
||||
FreeBSD, Mocktail 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.
|
||||
|
||||
@@ -143,7 +146,7 @@ sudo apt install build-essential cmake git ninja-build pkg-config lld \
|
||||
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
|
||||
libplacebo-dev libpng-dev zlib1g-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
"version_name": "2.721.1108",
|
||||
"version_code": 2350,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
|
||||
"status": "legacy-researched",
|
||||
"default_allowed": false,
|
||||
@@ -15,6 +16,7 @@
|
||||
{
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -35,6 +37,7 @@
|
||||
{
|
||||
"version_name": "2.727.1199",
|
||||
"version_code": 2628,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -46,6 +49,7 @@
|
||||
{
|
||||
"version_name": "2.734.917",
|
||||
"version_code": 2908,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -66,6 +70,19 @@
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"abi": "arm64-v8a",
|
||||
"elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching."
|
||||
},
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
|
||||
@@ -33,6 +33,23 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled);
|
||||
// guest layout explicit instead of exposing the incompatible host libc type.
|
||||
using MocktailBionicMutexAttr = int64_t;
|
||||
|
||||
// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host
|
||||
// glibc layout is opaque and wider on aarch64, so guest buffers must be read
|
||||
// and written through this struct only.
|
||||
struct MocktailBionicPthreadAttr {
|
||||
std::uint32_t flags;
|
||||
void* stack_base;
|
||||
std::size_t stack_size;
|
||||
std::size_t guard_size;
|
||||
std::int32_t sched_policy;
|
||||
std::int32_t sched_priority;
|
||||
char reserved[16];
|
||||
};
|
||||
static_assert(sizeof(MocktailBionicPthreadAttr) == 56);
|
||||
|
||||
inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1;
|
||||
inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2;
|
||||
|
||||
extern "C" {
|
||||
|
||||
void mocktail_set_current_jni_env(void* env);
|
||||
@@ -84,6 +101,19 @@ int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
|
||||
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
|
||||
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
|
||||
|
||||
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
|
||||
size_t stack_size);
|
||||
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
|
||||
int detach_state);
|
||||
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
|
||||
const struct sched_param* parameters);
|
||||
int mocktail_pthread_getattr_np(pthread_t thread,
|
||||
MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
|
||||
void** stack_base, size_t* stack_size);
|
||||
|
||||
extern void* mocktail_gameactivity_on_start_native;
|
||||
extern void* mocktail_gameactivity_on_resume_native;
|
||||
extern void* mocktail_gameactivity_on_surface_created_native;
|
||||
|
||||
@@ -25,11 +25,12 @@ void ConfigureBionicPthreadThreadInitializer(
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
struct MocktailBionicPthreadAttr;
|
||||
|
||||
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument);
|
||||
extern "C" int mocktail_bionic_pthread_create(
|
||||
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument);
|
||||
|
||||
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
|
||||
extern "C" int mocktail_bionic_pthread_setschedparam(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
|
||||
#include <elf.h>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// Android ABI name as it appears in APK lib/ directories and build metadata.
|
||||
inline constexpr std::string_view kGuestAbi = "arm64-v8a";
|
||||
// APK split-config name carrying the guest native libraries.
|
||||
inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a";
|
||||
// File name the payload store uses for the split APK.
|
||||
inline constexpr std::string_view kGuestSplitApkFile =
|
||||
"split_config.arm64_v8a.apk";
|
||||
// sha256 map key holding the split APK digest in payload metadata.
|
||||
inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk";
|
||||
// Short CPU label for device metadata and user-facing strings.
|
||||
inline constexpr std::string_view kGuestCpuName = "aarch64";
|
||||
inline constexpr int kGuestElfMachine = EM_AARCH64;
|
||||
#elif defined(__x86_64__)
|
||||
inline constexpr std::string_view kGuestAbi = "x86_64";
|
||||
inline constexpr std::string_view kGuestApkSplitName = "config.x86_64";
|
||||
inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk";
|
||||
inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk";
|
||||
inline constexpr std::string_view kGuestCpuName = "x86_64";
|
||||
inline constexpr int kGuestElfMachine = EM_X86_64;
|
||||
#else
|
||||
#error "mocktail only supports aarch64 and x86_64 hosts"
|
||||
#endif
|
||||
|
||||
// APK zip entry holding the guest libroblox.so.
|
||||
inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/";
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
@@ -48,9 +48,15 @@ struct PlatformIdentity {
|
||||
std::string manufacturer = "Mocktail";
|
||||
std::string model = "Mocktail Headless";
|
||||
std::string brand = "Mocktail";
|
||||
#if defined(__aarch64__)
|
||||
std::string device_code = "linux-arm64";
|
||||
std::string device_sku = "mocktail-arm64";
|
||||
std::string soc_model = "aarch64";
|
||||
#else
|
||||
std::string device_code = "linux-x86_64";
|
||||
std::string device_sku = "mocktail-x86_64";
|
||||
std::string soc_model = "x86_64";
|
||||
#endif
|
||||
};
|
||||
|
||||
struct RobloxCredentialView {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#if defined(__x86_64__)
|
||||
#include <xmmintrin.h>
|
||||
#endif
|
||||
#if defined(__aarch64__)
|
||||
#include <cstdint>
|
||||
#endif
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
@@ -22,16 +25,23 @@ class FmodThreadFloatingPointMode final {
|
||||
delete;
|
||||
|
||||
bool Enable(const char* thread_name) noexcept {
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__x86_64__) || defined(__aarch64__)
|
||||
if (active_ || thread_name == nullptr) return false;
|
||||
const std::string_view name(thread_name);
|
||||
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
|
||||
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
|
||||
return false;
|
||||
}
|
||||
#if defined(__x86_64__)
|
||||
const unsigned int control = _mm_getcsr();
|
||||
original_mode_ = control & kModeMask;
|
||||
_mm_setcsr(control | kModeMask);
|
||||
#else
|
||||
std::uint64_t control;
|
||||
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
|
||||
original_mode_ = control & kModeMask;
|
||||
__asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask));
|
||||
#endif
|
||||
active_ = true;
|
||||
return true;
|
||||
#else
|
||||
@@ -41,11 +51,18 @@ class FmodThreadFloatingPointMode final {
|
||||
}
|
||||
|
||||
void Restore() noexcept {
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__x86_64__) || defined(__aarch64__)
|
||||
if (active_) {
|
||||
// Restore only our two bits, preserving rounding, exception masks and
|
||||
// status flags set by the guest while the thread was attached.
|
||||
#if defined(__x86_64__)
|
||||
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
|
||||
#else
|
||||
std::uint64_t control;
|
||||
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
|
||||
__asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) |
|
||||
original_mode_));
|
||||
#endif
|
||||
active_ = false;
|
||||
}
|
||||
#endif
|
||||
@@ -56,6 +73,13 @@ class FmodThreadFloatingPointMode final {
|
||||
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
|
||||
unsigned int original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#elif defined(__aarch64__)
|
||||
// FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision
|
||||
// path used by some voice DSP filters.
|
||||
static constexpr std::uint64_t kModeMask =
|
||||
(std::uint64_t{1} << 24) | (std::uint64_t{1} << 19);
|
||||
std::uint64_t original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
enum class EtcFormat {
|
||||
kEtc2Rgb8,
|
||||
kEtc2Rgb8A1,
|
||||
kEtc2Rgba8,
|
||||
kEacR11,
|
||||
kEacR11Signed,
|
||||
kEacRg11,
|
||||
kEacRg11Signed,
|
||||
};
|
||||
|
||||
std::size_t EtcBlockBytes(EtcFormat format);
|
||||
|
||||
std::size_t EtcDecodedTexelBytes(EtcFormat format);
|
||||
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
struct EtcDecodeJob {
|
||||
EtcFormat format = EtcFormat::kEtc2Rgb8;
|
||||
const std::uint8_t* source = nullptr;
|
||||
std::size_t source_bytes = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint8_t* destination = nullptr;
|
||||
std::size_t destination_bytes = 0;
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// Synchronous; job destination buffers must not overlap.
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count);
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
@@ -0,0 +1,55 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
#define MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
struct RgbaImage {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size);
|
||||
std::string HashName(std::uint64_t hash);
|
||||
|
||||
bool ReadPngRgba(const std::string& path, RgbaImage* image);
|
||||
bool WritePngRgba(const std::string& path, const RgbaImage& image);
|
||||
|
||||
void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
|
||||
class TextureOverrides {
|
||||
public:
|
||||
TextureOverrides(std::string override_dir, std::string dump_dir);
|
||||
static TextureOverrides FromEnvironment();
|
||||
|
||||
bool enabled() const { return !override_dir_.empty() || !dump_dir_.empty(); }
|
||||
bool dumping() const { return !dump_dir_.empty(); }
|
||||
|
||||
std::shared_ptr<const RgbaImage> Lookup(std::uint64_t hash);
|
||||
void Dump(std::uint64_t hash, std::uint32_t width, std::uint32_t height,
|
||||
const std::uint8_t* rgba);
|
||||
|
||||
private:
|
||||
std::string override_dir_;
|
||||
std::string dump_dir_;
|
||||
std::mutex mutex_;
|
||||
std::unordered_map<std::uint64_t, std::shared_ptr<const RgbaImage>> cache_;
|
||||
std::unordered_set<std::uint64_t> dumped_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
@@ -0,0 +1,102 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
#define MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
struct Etc2EmulatedFormat {
|
||||
VkFormat host_format = VK_FORMAT_UNDEFINED;
|
||||
EtcFormat etc_format = EtcFormat::kEtc2Rgb8;
|
||||
};
|
||||
|
||||
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out);
|
||||
|
||||
VkFormatFeatureFlags EmulatedEtc2FormatFeatures(
|
||||
VkFormatFeatureFlags host_features);
|
||||
|
||||
bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t layer_count, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded);
|
||||
|
||||
bool Etc2SupportAdvertised(const char* value);
|
||||
bool Etc2SupportAdvertised();
|
||||
|
||||
inline constexpr std::uint32_t kSmallTextureMaxExtent = 64;
|
||||
inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4;
|
||||
std::uint32_t SmallTextureUpscale(const char* value);
|
||||
|
||||
class VulkanEtc2Emulation final {
|
||||
public:
|
||||
VulkanEtc2Emulation();
|
||||
~VulkanEtc2Emulation();
|
||||
VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete;
|
||||
VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete;
|
||||
|
||||
bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device,
|
||||
PFN_vkGetPhysicalDeviceFeatures host);
|
||||
|
||||
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
|
||||
bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image);
|
||||
void DestroyImage(VkDevice device, VkImage image,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
VkResult CreateImageView(VkDevice device,
|
||||
const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkImageView* view);
|
||||
VkResult BindBufferMemory(VkDevice device, VkBuffer buffer,
|
||||
VkDeviceMemory memory, VkDeviceSize offset);
|
||||
VkResult BindBufferMemory2(VkDevice device, std::uint32_t count,
|
||||
const VkBindBufferMemoryInfo* infos);
|
||||
VkResult MapMemory(VkDevice device, VkDeviceMemory memory,
|
||||
VkDeviceSize offset, VkDeviceSize size,
|
||||
VkMemoryMapFlags flags, void** data);
|
||||
VkResult MapMemory2(VkDevice device, const VkMemoryMapInfo* info,
|
||||
void** data);
|
||||
void UnmapMemory(VkDevice device, VkDeviceMemory memory);
|
||||
VkResult UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo* info);
|
||||
void FreeMemory(VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void DestroyBuffer(VkDevice device, VkBuffer buffer,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void CmdCopyBufferToImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkBuffer source, VkImage destination,
|
||||
VkImageLayout layout, std::uint32_t region_count,
|
||||
const VkBufferImageCopy* regions);
|
||||
void CmdCopyBufferToImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyBufferToImageInfo2* info);
|
||||
void CmdCopyImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkImage source, VkImageLayout source_layout,
|
||||
VkImage destination, VkImageLayout destination_layout,
|
||||
std::uint32_t region_count, const VkImageCopy* regions);
|
||||
void CmdCopyImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyImageInfo2* info);
|
||||
void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer,
|
||||
std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries);
|
||||
|
||||
// Resubmission must reread the source bytes.
|
||||
void PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count);
|
||||
// Call only after the command buffer is no longer pending.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
struct State;
|
||||
|
||||
private:
|
||||
State* state_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
#define MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DeviceMemoryProfile {
|
||||
std::int32_t total_memory_mb = 0;
|
||||
std::int32_t memory_class_mb = 0;
|
||||
std::int32_t large_memory_class_mb = 0;
|
||||
std::int64_t low_memory_killer_background_threshold = 0;
|
||||
std::int64_t low_memory_killer_foreground_threshold = 0;
|
||||
bool low_ram_device = false;
|
||||
};
|
||||
|
||||
DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
#define MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DisplaySize {
|
||||
std::int32_t width = 1920;
|
||||
std::int32_t height = 1080;
|
||||
};
|
||||
|
||||
// Physical pixels; window size is captured at startup.
|
||||
inline constexpr const char* kDisplaySizeEnvironment =
|
||||
"MOCKTAIL_DISPLAY_SIZE_INTERNAL";
|
||||
inline constexpr const char* kWindowSizeEnvironment =
|
||||
"MOCKTAIL_WINDOW_SIZE_INTERNAL";
|
||||
|
||||
inline DisplaySize ParseDisplaySize(const char* value) {
|
||||
const DisplaySize fallback;
|
||||
if (value == nullptr) {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parts[2] = {0, 0};
|
||||
const char* cursor = value;
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
if (*cursor < '0' || *cursor > '9') {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parsed = 0;
|
||||
while (*cursor >= '0' && *cursor <= '9') {
|
||||
parsed = parsed * 10 + (*cursor - '0');
|
||||
if (parsed > std::numeric_limits<std::int32_t>::max()) {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
if (parsed == 0) {
|
||||
return fallback;
|
||||
}
|
||||
parts[index] = parsed;
|
||||
if (index == 0) {
|
||||
if (*cursor != 'x') {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
if (*cursor != '\0') {
|
||||
return fallback;
|
||||
}
|
||||
return {static_cast<std::int32_t>(parts[0]),
|
||||
static_cast<std::int32_t>(parts[1])};
|
||||
}
|
||||
|
||||
inline std::int32_t PixelsToMillimetersAt160Dpi(std::int32_t pixels) {
|
||||
return static_cast<std::int32_t>(static_cast<std::int64_t>(pixels) * 254 /
|
||||
1600);
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
@@ -19,6 +19,13 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
|
||||
Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
// Converts a WebView launchGame command
|
||||
// ({"functionName":"launchGame","params":{"request":{...}}}) into the same
|
||||
// contract. Returns kUnsupported for other commands and kInvalidArgument for
|
||||
// a malformed launchGame request.
|
||||
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ struct ActivePayloadPaths {
|
||||
std::filesystem::path root;
|
||||
std::filesystem::path roblox_library;
|
||||
std::filesystem::path base_apk;
|
||||
std::filesystem::path x86_64_split_apk;
|
||||
std::filesystem::path arch_split_apk;
|
||||
std::filesystem::path assets_content;
|
||||
std::filesystem::path compatibility_manifest;
|
||||
std::filesystem::path host_abi_profile;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Allowed reduced aarch64 packages from pkgforge-dev/archlinux-pkgs-debloated.
|
||||
# The installer verifies the rolling release asset IDs and SHA256 digests.
|
||||
ffmpeg-mini-aarch64.pkg.tar.zst
|
||||
gdk-pixbuf2-mini-aarch64.pkg.tar.xz
|
||||
gtk4-mini-aarch64.pkg.tar.zst
|
||||
icu-mini-aarch64.pkg.tar.zst
|
||||
librsvg-mini-aarch64.pkg.tar.zst
|
||||
libxml2-mini-aarch64.pkg.tar.zst
|
||||
mesa-mini-aarch64.pkg.tar.zst
|
||||
opus-mini-aarch64.pkg.tar.zst
|
||||
@@ -1,11 +0,0 @@
|
||||
# pkgforge-dev/archlinux-pkgs-debloated, continuous release, 2026-09-12.
|
||||
# These assets are rolling. Refresh names/hashes together; do not bypass a
|
||||
# checksum mismatch. Only the disposable AppImage build container installs them.
|
||||
0bc7a050142b9e39e85035171c4c7a35ded6cada85e541ada8a04c812847a4b3 ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
7d033d304468bcf71798e93ef482f6eee9751ec73d9fce2d119714425c8dfa9e gdk-pixbuf2-mini-x86_64.pkg.tar.zst
|
||||
ca18bc00a343650cde3d674135fa482b54faa639017f82792906db8c68ca9fb6 gtk4-mini-x86_64.pkg.tar.zst
|
||||
5c178d1d0be438fda3f87efdb3d65eddf68187738fdfe1e1c563e46ce540ac8b icu-mini-x86_64.pkg.tar.zst
|
||||
3d51ee49d0e49a065971703b3f61b610bd1c307cd5165ec16846d75f3efe4a08 librsvg-mini-x86_64.pkg.tar.zst
|
||||
217bbd71516e8d0678d9c1c052a4370e07ca05525cfe9f8f844837a2554a03fc libxml2-mini-x86_64.pkg.tar.zst
|
||||
1952bc61dcbf7c880203c67d0df2b8fe4f77805abd0fe34f7552e87b8567fd3e mesa-mini-x86_64.pkg.tar.zst
|
||||
8c24921b4c4fe8b0e08ccdeb6c46e755fafa9f3665ab98ad77fc37dc048732b0 opus-mini-x86_64.pkg.tar.zst
|
||||
@@ -0,0 +1,12 @@
|
||||
# Allowed reduced packages from pkgforge-dev/archlinux-pkgs-debloated.
|
||||
# continuous is rolling: the installer snapshots GitHub asset IDs and SHA256
|
||||
# digests together at build time, and verifies every archive before installation.
|
||||
# Only the disposable AppImage build container installs these packages.
|
||||
ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
gdk-pixbuf2-mini-x86_64.pkg.tar.zst
|
||||
gtk4-mini-x86_64.pkg.tar.zst
|
||||
icu-mini-x86_64.pkg.tar.zst
|
||||
librsvg-mini-x86_64.pkg.tar.zst
|
||||
libxml2-mini-x86_64.pkg.tar.zst
|
||||
mesa-mini-x86_64.pkg.tar.zst
|
||||
opus-mini-x86_64.pkg.tar.zst
|
||||
@@ -16,6 +16,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
'minizip'
|
||||
|
||||
@@ -17,6 +17,7 @@ pkgbase = mocktail-bin
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
|
||||
@@ -24,6 +24,7 @@ pkgbase = mocktail-git
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
|
||||
@@ -24,6 +24,7 @@ pkgbase = mocktail
|
||||
depends = libelf
|
||||
depends = libglvnd
|
||||
depends = libplacebo
|
||||
depends = libpng
|
||||
depends = libsoup3
|
||||
depends = libutf8proc
|
||||
depends = libyaml
|
||||
|
||||
@@ -20,6 +20,7 @@ depends=(
|
||||
'libelf'
|
||||
'libglvnd'
|
||||
'libplacebo'
|
||||
'libpng'
|
||||
'libsoup3'
|
||||
'libutf8proc'
|
||||
'libyaml'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,10 +6,11 @@ set -Eeuo pipefail
|
||||
umask 022
|
||||
|
||||
readonly REPOSITORY="${1:?Flatpak repository path is required}"
|
||||
readonly BUNDLE="${2:?Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${3:?GPG public key path is required}"
|
||||
readonly OUTPUT="${4:?Pages output path is required}"
|
||||
readonly NATIVE_REPOSITORIES="${5:?Native repositories path is required}"
|
||||
readonly X86_64_BUNDLE="${2:?x86_64 Flatpak bundle path is required}"
|
||||
readonly AARCH64_BUNDLE="${3:?aarch64 Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${4:?GPG public key path is required}"
|
||||
readonly OUTPUT="${5:?Pages output path is required}"
|
||||
readonly NATIVE_REPOSITORIES="${6:?Native repositories path is required}"
|
||||
readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
@@ -18,10 +19,12 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
printf 'Flatpak repository is missing or unsafe: %s\n' "${REPOSITORY}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -f "${BUNDLE}" && ! -L "${BUNDLE}" ]] || {
|
||||
printf 'Flatpak bundle is missing or unsafe: %s\n' "${BUNDLE}" >&2
|
||||
exit 1
|
||||
}
|
||||
for bundle in "${X86_64_BUNDLE}" "${AARCH64_BUNDLE}"; do
|
||||
[[ -f "${bundle}" && ! -L "${bundle}" ]] || {
|
||||
printf 'Flatpak bundle is missing or unsafe: %s\n' "${bundle}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[[ -s "${PUBLIC_KEY}" && ! -L "${PUBLIC_KEY}" ]] || {
|
||||
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
@@ -40,7 +43,8 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
|
||||
mkdir -p -- "${OUTPUT}"
|
||||
cp -a -- "${REPOSITORY}" "${OUTPUT}/repo"
|
||||
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
|
||||
install -m 0644 -- "${X86_64_BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
|
||||
install -m 0644 -- "${AARCH64_BUNDLE}" "${OUTPUT}/Mocktail-aarch64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
@@ -69,7 +73,7 @@ Title=Mocktail Nightly
|
||||
Url=${BASE_URL}/repo/
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Nightly builds of Mocktail
|
||||
Description=Signed x86_64 nightly builds published from the latest Mocktail main branch
|
||||
Description=Signed x86_64 and aarch64 nightly builds published from the latest Mocktail main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
EOF
|
||||
|
||||
@@ -461,6 +461,7 @@ LatestSupportedBootstrapProfile() {
|
||||
.version_code <= ($remote_version_code | tonumber)) and
|
||||
.status == "supported" and .default_allowed == true and
|
||||
.allow_legacy_binary_patches == false and
|
||||
(.abi // "x86_64") == "x86_64" and
|
||||
(.version_name | type) == "string" and
|
||||
(.version_name | length) > 0)] |
|
||||
group_by(.version_code) |
|
||||
@@ -495,6 +496,7 @@ SelectSupportedBootstrapFallback() {
|
||||
bootstrap_source="$(jq -er --argjson version_code "${fallback_version_code}" '
|
||||
[.sources[]? |
|
||||
select(.version_code == $version_code and
|
||||
(.abi // "x86_64") == "x86_64" and
|
||||
.provider == "uptodown")] |
|
||||
if length == 1 then .[0].provider else empty end
|
||||
' "${BOOTSTRAP_SOURCES_PATH}" 2>/dev/null || true)"
|
||||
|
||||
@@ -329,6 +329,7 @@ def run_trial(args, max_ctors, skipped_offsets, trial_id):
|
||||
log_path = args.log_dir / f"trial_{trial_id:03d}_max_{max_ctors}.log"
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"ROBLOX_LIB_PATH": str(args.libroblox),
|
||||
"MOCKTAIL_RUN_LIBROBLOX_CTORS": "1",
|
||||
"MOCKTAIL_LIBROBLOX_CTOR_POLICY": args.ctor_policy,
|
||||
"MOCKTAIL_GRAPHICS_BACKEND": args.graphics_backend,
|
||||
@@ -500,7 +501,7 @@ def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--project-root", type=Path, default=project_root)
|
||||
parser.add_argument("--run-script", type=Path,
|
||||
default=project_root / "scripts" / "run_sober.sh")
|
||||
default=project_root / "build" / "mocktail")
|
||||
parser.add_argument("--libroblox", type=Path,
|
||||
default=project_root / "rbx_bin" / "libroblox.so")
|
||||
parser.add_argument("--log-dir", type=Path,
|
||||
|
||||
@@ -15,7 +15,7 @@ Usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/build_flatpak.sh [OPTIONS]
|
||||
|
||||
Build and install Mocktail as a per-user x86-64 Flatpak.
|
||||
Build and install Mocktail as a per-user Flatpak for the host architecture.
|
||||
|
||||
Options:
|
||||
--build-dir DIR Builder output directory (default: build-flatpak).
|
||||
@@ -92,8 +92,11 @@ done
|
||||
[[ "${JOBS}" =~ ^[1-9][0-9]*$ && "${JOBS}" -le 256 ]] ||
|
||||
Die "--jobs must be an integer between 1 and 256"
|
||||
|
||||
[[ "$(uname -m)" == x86_64 ]] ||
|
||||
Die "the Roblox payload and Flatpak manifest currently require x86-64"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64|aarch64) ;;
|
||||
*) Die "Flatpak builds require an x86_64 or aarch64 Linux host" ;;
|
||||
esac
|
||||
command -v flatpak >/dev/null 2>&1 || Die "flatpak is required"
|
||||
|
||||
MANIFEST="$(realpath -e -- "${MANIFEST}")" ||
|
||||
@@ -122,7 +125,7 @@ CleanupStaleBuilderMounts
|
||||
|
||||
cd -- "${PROJECT_ROOT}"
|
||||
exec "${builder[@]}" \
|
||||
--arch=x86_64 \
|
||||
--arch="${HOST_ARCH}" \
|
||||
--jobs="${JOBS}" \
|
||||
--force-clean \
|
||||
--install-deps-from=flathub \
|
||||
|
||||
@@ -61,6 +61,7 @@ def supported_profiles(catalog: dict) -> list[dict]:
|
||||
if isinstance(profile, dict)
|
||||
and profile.get("status") == "supported"
|
||||
and profile.get("default_allowed") is True
|
||||
and profile.get("abi", ARCHITECTURE) == ARCHITECTURE
|
||||
and isinstance(profile.get("version_name"), str)
|
||||
and isinstance(profile.get("version_code"), int)
|
||||
]
|
||||
|
||||
@@ -4,36 +4,131 @@
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# The reduced packages are intentionally limited to the disposable builder.
|
||||
# Never replace a developer's desktop / graphics packages on the host.
|
||||
[[ -f /.dockerenv || -f /run/.containerenv ]] || {
|
||||
printf 'AnyLinux dependency installation requires a disposable container\n' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$(id -u)" == 0 ]] || {
|
||||
printf 'AnyLinux dependency installation requires container root\n' >&2
|
||||
AnyLinuxDependencyDie() {
|
||||
printf '[anylinux-dependencies] error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.sha256"
|
||||
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
|
||||
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
|
||||
readonly BASE_URL=https://github.com/pkgforge-dev/archlinux-pkgs-debloated/releases/download/continuous
|
||||
declare -a packages=()
|
||||
while read -r digest filename remainder; do
|
||||
[[ -n "${digest}" && "${digest}" != \#* ]] || continue
|
||||
[[ "${digest}" =~ ^[0-9a-f]{64}$ &&
|
||||
"${filename}" =~ ^[a-z0-9-]+-x86_64\.pkg\.tar\.zst$ &&
|
||||
-z "${remainder}" ]] || {
|
||||
printf 'Invalid AnyLinux dependency manifest entry\n' >&2
|
||||
exit 1
|
||||
}
|
||||
curl --fail --location --retry 3 --output "${DOWNLOADS}/${filename}" \
|
||||
"${BASE_URL}/${filename}"
|
||||
printf '%s %s\n' "${digest}" "${DOWNLOADS}/${filename}" |
|
||||
sha256sum --check --strict
|
||||
packages+=("${DOWNLOADS}/${filename}")
|
||||
done <"${MANIFEST}"
|
||||
(( ${#packages[@]} > 0 ))
|
||||
pacman -U --noconfirm --needed "${packages[@]}"
|
||||
AnyLinuxDependencyRequireContainer() {
|
||||
# Never replace a developer's desktop / graphics packages on the host.
|
||||
[[ -f /.dockerenv || -f /run/.containerenv ]] ||
|
||||
AnyLinuxDependencyDie 'installation requires a disposable container'
|
||||
[[ "$(id -u)" == 0 ]] ||
|
||||
AnyLinuxDependencyDie 'installation requires container root'
|
||||
}
|
||||
|
||||
AnyLinuxDependencySnapshot() {
|
||||
local manifest="$1" release="$2" host_arch="${3:-$(uname -m)}"
|
||||
local filename remainder names_json
|
||||
[[ "${host_arch}" == x86_64 || "${host_arch}" == aarch64 ]] ||
|
||||
AnyLinuxDependencyDie 'unsupported AppImage architecture'
|
||||
local -a names=()
|
||||
local -A seen=()
|
||||
while read -r filename remainder || [[ -n "${filename}" ]]; do
|
||||
[[ -n "${filename}" && "${filename}" != \#* ]] || continue
|
||||
[[ "${filename}" =~ ^[a-z0-9-]+-mini-${host_arch}\.pkg\.tar\.(zst|xz)$ &&
|
||||
-z "${remainder}" && -z "${seen[${filename}]:-}" ]] ||
|
||||
AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry'
|
||||
seen["${filename}"]=1
|
||||
names+=("${filename}")
|
||||
done <"${manifest}"
|
||||
(( ${#names[@]} > 0 )) || AnyLinuxDependencyDie 'empty dependency manifest'
|
||||
names_json="$(printf '%s\n' "${names[@]}" | jq --raw-input --slurp \
|
||||
'split("\n")[:-1]')"
|
||||
|
||||
# continuous assets are replaced on upstream rebuilds. Resolve all required
|
||||
# names and GitHub-computed digests in one response, then download by asset ID
|
||||
# rather than by rolling filename. These are per-build integrity checks, not
|
||||
# immutable version pins. Never accept absent digests or compute our own trust.
|
||||
jq --exit-status --raw-output --argjson names "${names_json}" \
|
||||
--arg base 'https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/assets/' '
|
||||
(if .tag_name != "continuous" or (.assets | type) != "array" then
|
||||
error("invalid continuous release metadata")
|
||||
else .assets end) as $assets |
|
||||
$names[] as $name |
|
||||
[$assets[] | select(.name == $name)] as $matches |
|
||||
if ($matches | length) != 1 then
|
||||
error("missing or duplicate dependency asset: " + $name)
|
||||
else $matches[0] end |
|
||||
if .state != "uploaded" or (.id | type) != "number" or
|
||||
.id <= 0 or .id != (.id | floor) or
|
||||
(.digest | type) != "string" then
|
||||
error("invalid dependency asset metadata: " + $name)
|
||||
elif (.digest | test("^sha256:[0-9a-f]{64}$") | not) or
|
||||
.url != ($base + (.id | tostring)) then
|
||||
error("invalid dependency digest or URL: " + $name)
|
||||
else
|
||||
[(.digest | ltrimstr("sha256:")), $name, .url] | @tsv
|
||||
end
|
||||
' "${release}" || AnyLinuxDependencyDie 'cannot resolve verified dependency assets'
|
||||
}
|
||||
|
||||
AnyLinuxDownloadDependencies() {
|
||||
local manifest="$1" downloads="$2" attempt digest filename url failed
|
||||
local -a curl_options=(
|
||||
--fail --silent --show-error --location --retry 3
|
||||
--proto '=https' --proto-redir '=https'
|
||||
--connect-timeout 15 --max-time 180
|
||||
--header 'X-GitHub-Api-Version: 2022-11-28'
|
||||
)
|
||||
if [[ -n "${GH_TOKEN:-}" ]]; then
|
||||
curl_options+=(--header "Authorization: Bearer ${GH_TOKEN}")
|
||||
fi
|
||||
for attempt in 1 2 3; do
|
||||
curl "${curl_options[@]}" --header 'Accept: application/vnd.github+json' \
|
||||
--output "${downloads}/release.json" \
|
||||
https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/tags/continuous ||
|
||||
AnyLinuxDependencyDie 'cannot fetch upstream release metadata'
|
||||
AnyLinuxDependencySnapshot "${manifest}" "${downloads}/release.json" \
|
||||
>"${downloads}/dependencies.tsv"
|
||||
: >"${downloads}/SHA256SUMS"
|
||||
failed=false
|
||||
while IFS=$'\t' read -r digest filename url; do
|
||||
printf '[anylinux-dependencies] downloading %s (SHA256 %s)\n' \
|
||||
"${filename}" "${digest}" >&2
|
||||
if ! curl "${curl_options[@]}" --header 'Accept: application/octet-stream' \
|
||||
--output "${downloads}/${filename}" "${url}"; then
|
||||
failed=true
|
||||
break
|
||||
fi
|
||||
printf '%s %s\n' "${digest}" "${filename}" >>"${downloads}/SHA256SUMS"
|
||||
done <"${downloads}/dependencies.tsv"
|
||||
if [[ "${failed}" == true ]]; then
|
||||
# An upstream upload can delete an ID between resolution and download.
|
||||
# Retry a complete snapshot, never mix old checksums with new filenames.
|
||||
printf '[anylinux-dependencies] download failed; refreshing snapshot (%s/3)\n' \
|
||||
"${attempt}" >&2
|
||||
continue
|
||||
fi
|
||||
(cd -- "${downloads}" && sha256sum --check --strict SHA256SUMS) ||
|
||||
AnyLinuxDependencyDie 'dependency checksum verification failed'
|
||||
return 0
|
||||
done
|
||||
AnyLinuxDependencyDie 'dependency downloads failed after 3 snapshots'
|
||||
}
|
||||
|
||||
AnyLinuxDependenciesMain() (
|
||||
AnyLinuxDependencyRequireContainer
|
||||
(( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported'
|
||||
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
local host_arch="$(uname -m)" manifest_name
|
||||
case "${host_arch}" in
|
||||
x86_64) manifest_name=anylinux-dependencies.txt ;;
|
||||
aarch64) manifest_name=anylinux-dependencies-aarch64.txt ;;
|
||||
*) AnyLinuxDependencyDie "unsupported AppImage architecture: ${host_arch}" ;;
|
||||
esac
|
||||
readonly MANIFEST="${ROOT}/packaging/${manifest_name}"
|
||||
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
|
||||
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
|
||||
AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}"
|
||||
local digest filename url
|
||||
local -a packages=()
|
||||
while IFS=$'\t' read -r digest filename url; do
|
||||
packages+=("${DOWNLOADS}/${filename}")
|
||||
done <"${DOWNLOADS}/dependencies.tsv"
|
||||
pacman -U --noconfirm --needed "${packages[@]}"
|
||||
)
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
AnyLinuxDependenciesMain "$@"
|
||||
fi
|
||||
@@ -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'
|
||||
|
||||
@@ -110,8 +110,14 @@ for elf in "$@"; do
|
||||
fi
|
||||
machine="$(LC_ALL=C readelf --file-header "${elf}" 2>/dev/null |
|
||||
sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')"
|
||||
if [[ "${machine}" != *X86-64* ]]; then
|
||||
printf 'ABI check: %s is not x86-64 (%s)\n' "${elf}" "${machine}" >&2
|
||||
case "$(uname -m)" in
|
||||
x86_64) expected_machine='X86-64' ;;
|
||||
aarch64) expected_machine='AArch64' ;;
|
||||
*) expected_machine='X86-64' ;;
|
||||
esac
|
||||
if [[ "${machine}" != *"${expected_machine}"* ]]; then
|
||||
printf 'ABI check: %s is not %s (%s)\n' "${elf}" "${expected_machine}" \
|
||||
"${machine}" >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
|
||||
+8
-3
@@ -10,7 +10,7 @@
|
||||
<body><main>
|
||||
<img src="../packaging/space.bigrat.mocktail.svg" alt="Mocktail logo">
|
||||
<h1>Mocktail</h1>
|
||||
<p>Play Roblox on Linux. Use the stable Flathub release, or try the latest nightly build from main.</p>
|
||||
<p>Play Roblox on Linux. Use the stable Flathub release, or try the latest nightly build from main. Nightly Flatpaks are available for x86_64 and aarch64; Roblox runtime support on aarch64 is experimental.</p>
|
||||
<a class="button" href="https://flathub.org/apps/space.bigrat.mocktail">Stable on Flathub</a>
|
||||
<a class="button secondary" href="https://mocktail.bigrat.space/mocktail.flatpakref">Nightly build</a>
|
||||
<div class="commands">
|
||||
@@ -19,6 +19,10 @@
|
||||
<p>Nightly</p>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
</div>
|
||||
<p class="links">
|
||||
<a href="Mocktail-x86_64.flatpak">x86_64 nightly Flatpak</a>
|
||||
<a href="Mocktail-aarch64.flatpak">aarch64 nightly Flatpak</a>
|
||||
</p>
|
||||
<h2>AUR packages</h2>
|
||||
<p>Available from the AUR. Choose one package:</p>
|
||||
<p class="links">
|
||||
@@ -51,7 +55,7 @@ sudo apt update</code>
|
||||
<code>sudo apt install mocktail-nightly</code>
|
||||
</div>
|
||||
<h2>Direct downloads</h2>
|
||||
<p>Prebuilt packages for Linux x86_64.</p>
|
||||
<p>Prebuilt Linux packages. DEB and RPM are for x86_64; the nightly AppImage also has an aarch64 build.</p>
|
||||
<p class="download-label">Stable</p>
|
||||
<p class="links">
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/latest/download/Mocktail-x86_64.AppImage">AppImage</a>
|
||||
@@ -60,7 +64,8 @@ sudo apt update</code>
|
||||
</p>
|
||||
<p class="download-label">Nightly</p>
|
||||
<p class="links">
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.AppImage">AppImage</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.AppImage">AppImage (x86_64)</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly-aarch64.AppImage">AppImage (aarch64)</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.deb">DEB</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.rpm">RPM</a>
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "compat/bionic_pthread_create_runtime.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "compat/http_client_spin_guard.h"
|
||||
#include "window/window.h"
|
||||
|
||||
@@ -31,6 +33,14 @@ const char* EnvironmentOr(const char* name, const char* fallback) {
|
||||
return value != nullptr && value[0] != '\0' ? value : fallback;
|
||||
}
|
||||
|
||||
const char* GuestAbiPropertyValue() {
|
||||
#if defined(__aarch64__)
|
||||
return "arm64-v8a";
|
||||
#else
|
||||
return "x86_64";
|
||||
#endif
|
||||
}
|
||||
|
||||
int CallHostVsnprintf(char* dst, size_t count, const char* format,
|
||||
va_list args) {
|
||||
using HostVsnprintf = int (*)(char*, size_t, const char*, va_list);
|
||||
@@ -173,8 +183,12 @@ int __system_property_get(const char* name, char* value) {
|
||||
result = "35";
|
||||
} else if (std::strcmp(name, "ro.build.version.release") == 0) {
|
||||
result = "15";
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0) {
|
||||
result = "x86_64";
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0 ||
|
||||
std::strcmp(name, "ro.product.cpu.abilist") == 0 ||
|
||||
std::strcmp(name, "ro.product.cpu.abilist64") == 0) {
|
||||
result = GuestAbiPropertyValue();
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abilist32") == 0) {
|
||||
result = "";
|
||||
} else if (std::strcmp(name, "ro.product.manufacturer") == 0) {
|
||||
result = EnvironmentOr("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
|
||||
} else if (std::strcmp(name, "ro.product.model") == 0) {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "compat/bionic_pthread_create_runtime.h"
|
||||
|
||||
#include "compat/bionic_abi_exports.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
|
||||
@@ -69,24 +73,28 @@ size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
|
||||
return guest_stack_size;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
// Guest pthread_attr_t buffers use the Bionic LP64 layout; they are never a
|
||||
// host pthread_attr_t, so decode the fixed fields directly.
|
||||
int GuestDetachState(const MocktailBionicPthreadAttr& source) noexcept {
|
||||
return (source.flags & kMocktailBionicAttrFlagDetached) != 0
|
||||
? PTHREAD_CREATE_DETACHED
|
||||
: PTHREAD_CREATE_JOINABLE;
|
||||
}
|
||||
|
||||
size_t GuestStackSize(const MocktailBionicPthreadAttr& source) noexcept {
|
||||
return source.stack_size != 0 ? source.stack_size
|
||||
: kBionicLp64DefaultThreadStackSize;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const MocktailBionicPthreadAttr& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(&source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t guard_size = 0;
|
||||
result = pthread_attr_getguardsize(&source, &guard_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setguardsize(destination, guard_size);
|
||||
result = pthread_attr_setguardsize(destination, source.guard_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -95,53 +103,38 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
// pthread_attr_setstack. Copying pthread_attr_getstack's address is unsafe:
|
||||
// glibc represents an automatically allocated stack with a synthetic
|
||||
// address derived from its size.
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstacksize(&source, &stack_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
result = pthread_attr_setstacksize(
|
||||
destination, HostStackSizeForGuest(GuestStackSize(source)));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int inherit_scheduler = PTHREAD_INHERIT_SCHED;
|
||||
result = pthread_attr_getinheritsched(&source, &inherit_scheduler);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
// Bionic applies the attribute's scheduling policy at creation whenever it
|
||||
// differs from the normal policy.
|
||||
if (RejectRealtimeScheduling(source.sched_policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
if (inherit_scheduler == PTHREAD_EXPLICIT_SCHED) {
|
||||
int scheduler_policy = 0;
|
||||
sched_param scheduler_parameters{};
|
||||
result = pthread_attr_getschedpolicy(&source, &scheduler_policy);
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getschedparam(&source, &scheduler_parameters);
|
||||
}
|
||||
if (result == 0 && RejectRealtimeScheduling(scheduler_policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
if (result == 0) {
|
||||
result = pthread_attr_setschedpolicy(destination, scheduler_policy);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
|
||||
}
|
||||
if (source.sched_policy != SCHED_NORMAL) {
|
||||
result = pthread_attr_setschedpolicy(destination, source.sched_policy);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
sched_param scheduler_parameters{};
|
||||
scheduler_parameters.sched_priority = source.sched_priority;
|
||||
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setinheritsched(destination, PTHREAD_EXPLICIT_SCHED);
|
||||
}
|
||||
return pthread_attr_setinheritsched(destination, inherit_scheduler);
|
||||
|
||||
return pthread_attr_setinheritsched(destination, PTHREAD_INHERIT_SCHED);
|
||||
}
|
||||
|
||||
int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
int CopyRequiredThreadAttributes(const MocktailBionicPthreadAttr& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(&source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -149,32 +142,20 @@ int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
// The retry must retain an explicitly requested guest stack. Falling back
|
||||
// to a null/default host attr is unsafe on musl, whose default stack is much
|
||||
// smaller than Bionic's and can be exhausted by a single libroblox frame.
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstacksize(&source, &stack_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
return pthread_attr_setstacksize(
|
||||
destination, HostStackSizeForGuest(GuestStackSize(source)));
|
||||
}
|
||||
|
||||
int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
int ConfigureHostSafeStackFallback(const MocktailBionicPthreadAttr* source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
size_t guest_stack_size = 0;
|
||||
if (source != nullptr) {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_getstacksize(source, &guest_stack_size);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(*source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
guest_stack_size = GuestStackSize(*source);
|
||||
}
|
||||
|
||||
size_t host_default_stack_size = 0;
|
||||
@@ -207,7 +188,8 @@ void ConfigureBionicPthreadThreadInitializer(
|
||||
g_thread_initializer.store(initializer, std::memory_order_release);
|
||||
}
|
||||
|
||||
int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
int CreateBionicPthread(pthread_t* thread,
|
||||
const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument) {
|
||||
if (thread == nullptr || start_routine == nullptr) {
|
||||
return EINVAL;
|
||||
@@ -289,10 +271,124 @@ int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument) {
|
||||
extern "C" {
|
||||
|
||||
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->flags = 0;
|
||||
attr->stack_base = nullptr;
|
||||
attr->stack_size =
|
||||
mocktail::compat::kBionicLp64DefaultThreadStackSize;
|
||||
const long page_size = sysconf(_SC_PAGESIZE);
|
||||
attr->guard_size = page_size > 0 ? static_cast<size_t>(page_size) : 0;
|
||||
attr->sched_policy = SCHED_OTHER;
|
||||
attr->sched_priority = 0;
|
||||
std::memset(attr->reserved, 0, sizeof(attr->reserved));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr) {
|
||||
(void)attr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
|
||||
size_t stack_size) {
|
||||
if (attr == nullptr || stack_size < PTHREAD_STACK_MIN) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->stack_size = stack_size;
|
||||
attr->flags &= ~kMocktailBionicAttrFlagUserStack;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
|
||||
int detach_state) {
|
||||
if (attr == nullptr || (detach_state != PTHREAD_CREATE_DETACHED &&
|
||||
detach_state != PTHREAD_CREATE_JOINABLE)) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (detach_state == PTHREAD_CREATE_DETACHED) {
|
||||
attr->flags |= kMocktailBionicAttrFlagDetached;
|
||||
} else {
|
||||
attr->flags &= ~kMocktailBionicAttrFlagDetached;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
|
||||
const struct sched_param* parameters) {
|
||||
if (attr == nullptr || parameters == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->sched_priority = parameters->sched_priority;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_getattr_np(pthread_t thread,
|
||||
MocktailBionicPthreadAttr* attr) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
pthread_attr_t host_attr;
|
||||
int result = pthread_getattr_np(thread, &host_attr);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
void* stack_base = nullptr;
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstack(&host_attr, &stack_base, &stack_size);
|
||||
size_t guard_size = 0;
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getguardsize(&host_attr, &guard_size);
|
||||
}
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getdetachstate(&host_attr, &detach_state);
|
||||
}
|
||||
sched_param parameters{};
|
||||
int policy = SCHED_OTHER;
|
||||
if (result == 0) {
|
||||
result = pthread_getschedparam(thread, &policy, ¶meters);
|
||||
}
|
||||
pthread_attr_destroy(&host_attr);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
attr->flags = detach_state == PTHREAD_CREATE_DETACHED
|
||||
? kMocktailBionicAttrFlagDetached
|
||||
: 0;
|
||||
attr->stack_base = stack_base;
|
||||
attr->stack_size = stack_size;
|
||||
attr->guard_size = guard_size;
|
||||
attr->sched_policy = policy;
|
||||
attr->sched_priority = parameters.sched_priority;
|
||||
std::memset(attr->reserved, 0, sizeof(attr->reserved));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
|
||||
void** stack_base, size_t* stack_size) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (stack_base != nullptr) {
|
||||
*stack_base = attr->stack_base;
|
||||
}
|
||||
if (stack_size != nullptr) {
|
||||
*stack_size = attr->stack_size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
} // extern "C"
|
||||
|
||||
extern "C" int mocktail_bionic_pthread_create(
|
||||
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument) {
|
||||
return mocktail::compat::CreateBionicPthread(thread, attr, start_routine,
|
||||
argument);
|
||||
}
|
||||
|
||||
@@ -39,12 +39,23 @@ bool PatchAbsoluteJump(uintptr_t address, void *target) noexcept {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// ldr x16, #8 ; br x16 ; .quad target
|
||||
// x16 is the intra-procedure-call scratch register, free to clobber.
|
||||
unsigned char patch[] = {
|
||||
0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
|
||||
std::memcpy(patch + 8, &target_address, sizeof(target_address));
|
||||
#else
|
||||
unsigned char patch[] = {
|
||||
0x48, 0xb8, // movabs rax, imm64
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0, // jmp rax
|
||||
};
|
||||
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
|
||||
std::memcpy(patch + 2, &target_address, sizeof(target_address));
|
||||
#endif
|
||||
|
||||
const uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
|
||||
const uintptr_t page = address & ~page_mask;
|
||||
|
||||
@@ -6,6 +6,34 @@ namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
constexpr HostAbiProfile kProfiles[] = {
|
||||
// Roblox 2.736.1408 arm64-v8a. Every RVA was derived from the AArch64
|
||||
// payload of the same release as the x86-64 reference above, matched by
|
||||
// allocator internals, init_array call graphs, and static state layout.
|
||||
// Constructor policy mirrors x86-64 by semantic role: the pre-init
|
||||
// entries 0..3 stay skipped, index 4 is mi_process_load (thread init
|
||||
// checkpoint), index 5 is allocator wiring and index 6 is the lazy clock
|
||||
// singleton getter, both excluded under host bridges only.
|
||||
{"ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
|
||||
{{{0x1d53840, HostBridgeKind::kAllocate, "small-allocate"},
|
||||
{0x1d56c34, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x1d6ef80, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1d53568, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x61d3850, HostBridgeKind::kAlignedAllocate,
|
||||
"aligned-allocate-direct"},
|
||||
{0x1d57328, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0, 0, 0x7296c88, 0x400, 0x1d53c54, 0x1dc76a0,
|
||||
0x71d6c90, 0x71d2c40, 0x400000},
|
||||
{0x1d53568, 0x1d57328},
|
||||
0x6733128,
|
||||
3565,
|
||||
{{{4, 5}, {7, 3565}}},
|
||||
2,
|
||||
{{{4, 3565}}},
|
||||
1,
|
||||
4,
|
||||
{0x2132c5c, 0x71d6d30},
|
||||
HostAllocatorStrategy::kNativeMimalloc},
|
||||
// Roblox 2.736.1408. Derived from the verified 2908 profile and validated
|
||||
// by two isolated no-recovery Vulkan canaries and real game sessions.
|
||||
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::compat {
|
||||
namespace {
|
||||
@@ -611,19 +612,26 @@ std::unique_ptr<OwnedHostAbiProfile> ParseProfile(const Json& root,
|
||||
*error = "external host ABI profile has inconsistent constructor policy";
|
||||
return nullptr;
|
||||
}
|
||||
if (owned->profile.init_array_count <= 5 ||
|
||||
// The verified constructor layout is the same on both guest ABIs: a single
|
||||
// process-load constructor at the thread-initializer checkpoint, two wiring
|
||||
// entries excluded under host bridges, and one contiguous native-mimalloc
|
||||
// replay from the checkpoint to the end of .init_array. Only the checkpoint
|
||||
// index itself differs between payloads.
|
||||
const std::size_t checkpoint =
|
||||
owned->profile.native_mimalloc_thread_initializer_after_constructor;
|
||||
if (owned->profile.init_array_count <= checkpoint + 3 ||
|
||||
owned->profile.constructor_run_range_count != 2 ||
|
||||
owned->profile.constructor_run_ranges[0].begin != 2 ||
|
||||
owned->profile.constructor_run_ranges[0].end_exclusive != 3 ||
|
||||
owned->profile.constructor_run_ranges[1].begin != 5 ||
|
||||
owned->profile.constructor_run_ranges[0].begin != checkpoint ||
|
||||
owned->profile.constructor_run_ranges[0].end_exclusive != checkpoint + 1 ||
|
||||
owned->profile.constructor_run_ranges[1].begin != checkpoint + 3 ||
|
||||
owned->profile.constructor_run_ranges[1].end_exclusive !=
|
||||
owned->profile.init_array_count ||
|
||||
owned->profile.native_mimalloc_constructor_run_range_count != 1 ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin != 2 ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin !=
|
||||
checkpoint ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].end_exclusive !=
|
||||
owned->profile.init_array_count ||
|
||||
owned->profile.native_mimalloc_thread_initializer_after_constructor !=
|
||||
2) {
|
||||
(kGuestElfMachine == EM_X86_64 && checkpoint != 2)) {
|
||||
*error =
|
||||
"external host ABI profile changed the verified constructor policy";
|
||||
return nullptr;
|
||||
@@ -669,9 +677,10 @@ bool ValidateProfileAgainstElf(const std::string& payload_path,
|
||||
}
|
||||
GElf_Ehdr header{};
|
||||
if (gelf_getehdr(elf.get(), &header) == nullptr ||
|
||||
gelf_getclass(elf.get()) != ELFCLASS64 || header.e_machine != EM_X86_64 ||
|
||||
header.e_type != ET_DYN) {
|
||||
*error = "payload must be an x86-64 ET_DYN ELF object";
|
||||
gelf_getclass(elf.get()) != ELFCLASS64 ||
|
||||
header.e_machine != kGuestElfMachine || header.e_type != ET_DYN) {
|
||||
*error = "payload must be a " + std::string(kGuestAbi) +
|
||||
" ET_DYN ELF object";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Includes changes by vii from CoderDayton/nightcap.
|
||||
// Android Vulkan loader ABI -> host Vulkan loader + SDL3 WSI.
|
||||
|
||||
#include <dlfcn.h>
|
||||
@@ -17,6 +18,7 @@
|
||||
|
||||
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
|
||||
#include "mocktail/graphics/present_mode_policy.h"
|
||||
#include "mocktail/graphics/vulkan_etc2_emulation.h"
|
||||
#include "mocktail/graphics/vulkan_text_overlay_compositor.h"
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
@@ -188,6 +190,7 @@ struct AdapterState {
|
||||
VkInstance latest_instance = VK_NULL_HANDLE;
|
||||
size_t active_instance_count = 0;
|
||||
mocktail::graphics::VulkanTextOverlayCompositor text_overlay;
|
||||
mocktail::graphics::VulkanEtc2Emulation etc2;
|
||||
std::atomic<NotePresentFn> note_present{nullptr};
|
||||
std::atomic<NoteHostPresentBeginFn> note_host_present_begin{nullptr};
|
||||
std::atomic<NoteHostPresentEndFn> note_host_present_end{nullptr};
|
||||
@@ -727,12 +730,90 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
MOCKTAIL_VK_PROC(vkQueueWaitIdle)
|
||||
MOCKTAIL_VK_PROC(vkDeviceWaitIdle)
|
||||
MOCKTAIL_VK_PROC(vkQueuePresentKHR)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures2)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties2)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties2)
|
||||
MOCKTAIL_VK_PROC(vkCreateImage)
|
||||
MOCKTAIL_VK_PROC(vkDestroyImage)
|
||||
MOCKTAIL_VK_PROC(vkCreateImageView)
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory)
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_PROC(vkMapMemory)
|
||||
MOCKTAIL_VK_PROC(vkMapMemory2)
|
||||
MOCKTAIL_VK_PROC(vkUnmapMemory)
|
||||
MOCKTAIL_VK_PROC(vkUnmapMemory2)
|
||||
MOCKTAIL_VK_PROC(vkFreeMemory)
|
||||
MOCKTAIL_VK_PROC(vkDestroyBuffer)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyImage)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyImage2)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage2)
|
||||
MOCKTAIL_VK_PROC(vkCmdExecuteCommands)
|
||||
#undef MOCKTAIL_VK_PROC
|
||||
#define MOCKTAIL_VK_ALIAS(alias, function) \
|
||||
if (std::strcmp(name, #alias) == 0) { \
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(function); \
|
||||
}
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFeatures2KHR,
|
||||
vkGetPhysicalDeviceFeatures2)
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFormatProperties2KHR,
|
||||
vkGetPhysicalDeviceFormatProperties2)
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceImageFormatProperties2KHR,
|
||||
vkGetPhysicalDeviceImageFormatProperties2)
|
||||
MOCKTAIL_VK_ALIAS(vkBindBufferMemory2KHR, vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyImage2KHR, vkCmdCopyImage2)
|
||||
MOCKTAIL_VK_ALIAS(vkMapMemory2KHR, vkMapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkUnmapMemory2KHR, vkUnmapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyBufferToImage2KHR, vkCmdCopyBufferToImage2)
|
||||
#undef MOCKTAIL_VK_ALIAS
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool NameInList(const char* name, const char* const* names,
|
||||
std::size_t count) {
|
||||
return name != nullptr &&
|
||||
std::any_of(names, names + count, [name](const char* candidate) {
|
||||
return std::strcmp(name, candidate) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
bool IsEtc2DeviceProc(const char* name) {
|
||||
static constexpr const char* kNames[] = {
|
||||
"vkCreateImage", "vkDestroyImage",
|
||||
"vkCreateImageView", "vkBindBufferMemory",
|
||||
"vkBindBufferMemory2", "vkBindBufferMemory2KHR",
|
||||
"vkMapMemory", "vkMapMemory2",
|
||||
"vkMapMemory2KHR", "vkUnmapMemory",
|
||||
"vkUnmapMemory2", "vkUnmapMemory2KHR",
|
||||
"vkFreeMemory", "vkDestroyBuffer",
|
||||
"vkCmdCopyBufferToImage", "vkCmdCopyBufferToImage2",
|
||||
"vkCmdCopyImage", "vkCmdCopyImage2",
|
||||
"vkCmdCopyImage2KHR",
|
||||
"vkCmdCopyBufferToImage2KHR", "vkCmdExecuteCommands",
|
||||
};
|
||||
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
|
||||
}
|
||||
|
||||
bool IsEtc2PhysicalDeviceProc(const char* name) {
|
||||
static constexpr const char* kNames[] = {
|
||||
"vkGetPhysicalDeviceFeatures",
|
||||
"vkGetPhysicalDeviceFeatures2",
|
||||
"vkGetPhysicalDeviceFeatures2KHR",
|
||||
"vkGetPhysicalDeviceFormatProperties",
|
||||
"vkGetPhysicalDeviceFormatProperties2",
|
||||
"vkGetPhysicalDeviceFormatProperties2KHR",
|
||||
"vkGetPhysicalDeviceImageFormatProperties",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2KHR",
|
||||
};
|
||||
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
|
||||
}
|
||||
|
||||
bool IsDeviceAdapterProc(const char* name) {
|
||||
return name != nullptr && (std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
return name != nullptr && (IsEtc2DeviceProc(name) || std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue2") == 0 ||
|
||||
std::strcmp(name, "vkCreateSwapchainKHR") == 0 ||
|
||||
@@ -959,11 +1040,23 @@ struct FpsWaitTrace {
|
||||
std::atomic<std::uint64_t> total_ns{0};
|
||||
std::atomic<std::uint64_t> max_ns{0};
|
||||
std::atomic<std::uint64_t> window_start_ns{0};
|
||||
std::atomic<std::uint64_t> max_gap_ns{0};
|
||||
std::atomic<std::uint64_t> last_start_ns{0};
|
||||
|
||||
void Record(std::uint64_t start_ns) {
|
||||
if (name == nullptr || start_ns == 0) {
|
||||
return;
|
||||
}
|
||||
const std::uint64_t previous_start =
|
||||
last_start_ns.exchange(start_ns, std::memory_order_relaxed);
|
||||
if (previous_start != 0 && start_ns > previous_start) {
|
||||
const std::uint64_t gap_ns = start_ns - previous_start;
|
||||
std::uint64_t max_gap = max_gap_ns.load(std::memory_order_relaxed);
|
||||
while (gap_ns > max_gap &&
|
||||
!max_gap_ns.compare_exchange_weak(max_gap, gap_ns,
|
||||
std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
const std::uint64_t wait_ns = MonotonicNanos() - start_ns;
|
||||
total_ns.fetch_add(wait_ns, std::memory_order_relaxed);
|
||||
std::uint64_t max_wait = max_ns.load(std::memory_order_relaxed);
|
||||
@@ -984,14 +1077,18 @@ struct FpsWaitTrace {
|
||||
const std::uint64_t total = total_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t peak = max_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t count = samples.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t gap_peak =
|
||||
max_gap_ns.exchange(0, std::memory_order_relaxed);
|
||||
window_start_ns.store(start_ns, std::memory_order_relaxed);
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
std::fprintf(stderr, " [fps] %s n=%llu avg=%llu us max=%llu us\n", name,
|
||||
static_cast<unsigned long long>(count),
|
||||
std::fprintf(stderr,
|
||||
" [fps] %s n=%llu avg=%llu us max=%llu us gap_max=%llu us\n",
|
||||
name, static_cast<unsigned long long>(count),
|
||||
static_cast<unsigned long long>(total / count / 1000ULL),
|
||||
static_cast<unsigned long long>(peak / 1000ULL));
|
||||
static_cast<unsigned long long>(peak / 1000ULL),
|
||||
static_cast<unsigned long long>(gap_peak / 1000ULL));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1042,6 +1139,58 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
Function HostPhysicalDeviceProc(const char* name, const char* alias) {
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
instance = state.latest_instance;
|
||||
}
|
||||
PFN_vkVoidFunction proc = HostInstanceProc(instance, name);
|
||||
if (proc == nullptr && alias != nullptr) {
|
||||
proc = HostInstanceProc(instance, alias);
|
||||
}
|
||||
return reinterpret_cast<Function>(proc);
|
||||
}
|
||||
|
||||
bool Etc2EmulatedPhysicalDevice(VkPhysicalDevice physical_device) {
|
||||
return State().etc2.PhysicalDeviceNeedsEmulation(
|
||||
physical_device, HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
"vkGetPhysicalDeviceFeatures", nullptr));
|
||||
}
|
||||
|
||||
void ReleaseEtc2PoolCommandBuffers(VkDevice device,
|
||||
VkCommandPool command_pool) {
|
||||
std::vector<VkCommandBuffer> command_buffers;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
for (const auto& binding : state.command_buffer_bindings) {
|
||||
if (binding.device == device && binding.command_pool == command_pool) {
|
||||
command_buffers.push_back(binding.command_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (VkCommandBuffer command_buffer : command_buffers) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits) {
|
||||
if (submits == nullptr) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -1247,7 +1396,7 @@ vkGetInstanceProcAddr(VkInstance instance, const char* name) {
|
||||
}
|
||||
if (const PFN_vkVoidFunction adapter = AdapterProc(name);
|
||||
adapter != nullptr) {
|
||||
if (IsDeviceAdapterProc(name) &&
|
||||
if ((IsDeviceAdapterProc(name) || IsEtc2PhysicalDeviceProc(name)) &&
|
||||
HostInstanceProc(instance, name) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1306,6 +1455,30 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
|
||||
VkDeviceCreateInfo host_info = *create_info;
|
||||
EnabledDeviceFeatures enabled = InspectEnabledFeatures(*create_info);
|
||||
const bool etc2_emulated = State().etc2.PhysicalDeviceNeedsEmulation(
|
||||
physical_device, reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
HostInstanceProc(instance,
|
||||
"vkGetPhysicalDeviceFeatures")));
|
||||
VkPhysicalDeviceFeatures host_enabled_features{};
|
||||
VkPhysicalDeviceFeatures2* requested_features2 = nullptr;
|
||||
VkBool32 requested_features2_etc2 = VK_FALSE;
|
||||
if (etc2_emulated) {
|
||||
enabled.root.features.textureCompressionETC2 = VK_FALSE;
|
||||
if (create_info->pEnabledFeatures != nullptr) {
|
||||
host_enabled_features = *create_info->pEnabledFeatures;
|
||||
host_enabled_features.textureCompressionETC2 = VK_FALSE;
|
||||
host_info.pEnabledFeatures = &host_enabled_features;
|
||||
}
|
||||
// Restore the caller-owned feature bit after the host call.
|
||||
requested_features2 = const_cast<VkPhysicalDeviceFeatures2*>(
|
||||
reinterpret_cast<const VkPhysicalDeviceFeatures2*>(FindFeature(
|
||||
create_info->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)));
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2_etc2 =
|
||||
requested_features2->features.textureCompressionETC2;
|
||||
requested_features2->features.textureCompressionETC2 = VK_FALSE;
|
||||
}
|
||||
}
|
||||
enabled.root.pNext = &enabled.vulkan11;
|
||||
enabled.vulkan11.pNext = &enabled.vulkan12;
|
||||
enabled.vulkan12.pNext = nullptr;
|
||||
@@ -1341,11 +1514,25 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
|
||||
const VkResult result =
|
||||
host_create(physical_device, &host_info, allocator, device);
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2->features.textureCompressionETC2 =
|
||||
requested_features2_etc2;
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
RegisterHostDeviceDispatch(*device, physical_device,
|
||||
host_get_device_proc_addr);
|
||||
VkPhysicalDeviceMemoryProperties memory_properties{};
|
||||
if (const auto host_memory_properties =
|
||||
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
HostInstanceProc(instance,
|
||||
"vkGetPhysicalDeviceMemoryProperties"));
|
||||
host_memory_properties != nullptr) {
|
||||
host_memory_properties(physical_device, &memory_properties);
|
||||
}
|
||||
State().etc2.RegisterDevice(*device, physical_device, etc2_emulated,
|
||||
memory_properties, host_get_device_proc_addr);
|
||||
|
||||
std::uint32_t queue_family_count = 0;
|
||||
const auto host_get_queue_families =
|
||||
@@ -1403,6 +1590,7 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) {
|
||||
const auto host_destroy = reinterpret_cast<PFN_vkDestroyDevice>(
|
||||
HostDeviceProc(device, "vkDestroyDevice"));
|
||||
State().text_overlay.DestroyDevice(device);
|
||||
State().etc2.DestroyDevice(device);
|
||||
if (host_destroy != nullptr) {
|
||||
host_destroy(device, allocator);
|
||||
}
|
||||
@@ -1650,6 +1838,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
ReleaseEtc2PoolCommandBuffers(device, command_pool);
|
||||
const VkResult result = host_reset(device, command_pool, flags);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1706,6 +1895,11 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
if (command_buffers != nullptr) {
|
||||
for (std::uint32_t index = 0; index < command_buffer_count; ++index) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffers[index]);
|
||||
}
|
||||
}
|
||||
host_free(device, command_pool, command_buffer_count, command_buffers);
|
||||
RemoveHostCommandBuffers(device, command_pool, command_buffer_count,
|
||||
command_buffers);
|
||||
@@ -1722,6 +1916,7 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
ReleaseEtc2PoolCommandBuffers(device, command_pool);
|
||||
host_destroy(device, command_pool, allocator);
|
||||
RemoveHostCommandPoolBindings(device, command_pool);
|
||||
observation.SetResult(VK_SUCCESS);
|
||||
@@ -1737,6 +1932,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
const VkResult result = host_begin(command_buffer, begin_info);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1765,6 +1961,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
const VkResult result = host_reset(command_buffer, flags);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1801,6 +1998,12 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
if (submits != nullptr) {
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
State().etc2.PrepareSubmit(submits[index].pCommandBuffers,
|
||||
submits[index].commandBufferCount);
|
||||
}
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1817,6 +2020,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1833,6 +2037,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence,
|
||||
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
|
||||
@@ -2025,4 +2230,250 @@ vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return normalized_result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures(
|
||||
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures* features) {
|
||||
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
"vkGetPhysicalDeviceFeatures", nullptr);
|
||||
if (host == nullptr || features == nullptr) {
|
||||
return;
|
||||
}
|
||||
host(physical_device, features);
|
||||
if (Etc2EmulatedPhysicalDevice(physical_device) &&
|
||||
mocktail::graphics::Etc2SupportAdvertised()) {
|
||||
features->textureCompressionETC2 = VK_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2(
|
||||
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures2* features) {
|
||||
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures2>(
|
||||
"vkGetPhysicalDeviceFeatures2", "vkGetPhysicalDeviceFeatures2KHR");
|
||||
if (host == nullptr || features == nullptr) {
|
||||
return;
|
||||
}
|
||||
host(physical_device, features);
|
||||
if (Etc2EmulatedPhysicalDevice(physical_device) &&
|
||||
mocktail::graphics::Etc2SupportAdvertised()) {
|
||||
features->features.textureCompressionETC2 = VK_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties(
|
||||
VkPhysicalDevice physical_device, VkFormat format,
|
||||
VkFormatProperties* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties>(
|
||||
"vkGetPhysicalDeviceFormatProperties", nullptr);
|
||||
if (host == nullptr || properties == nullptr) {
|
||||
return;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
host(physical_device, format, properties);
|
||||
return;
|
||||
}
|
||||
host(physical_device, mapped.host_format, properties);
|
||||
properties->linearTilingFeatures = 0;
|
||||
properties->optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(
|
||||
properties->optimalTilingFeatures);
|
||||
properties->bufferFeatures = 0;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2(
|
||||
VkPhysicalDevice physical_device, VkFormat format,
|
||||
VkFormatProperties2* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties2>(
|
||||
"vkGetPhysicalDeviceFormatProperties2",
|
||||
"vkGetPhysicalDeviceFormatProperties2KHR");
|
||||
if (host == nullptr || properties == nullptr) {
|
||||
return;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
host(physical_device, format, properties);
|
||||
return;
|
||||
}
|
||||
host(physical_device, mapped.host_format, properties);
|
||||
VkFormatProperties& base = properties->formatProperties;
|
||||
base.linearTilingFeatures = 0;
|
||||
base.optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(base.optimalTilingFeatures);
|
||||
base.bufferFeatures = 0;
|
||||
if (auto* extended = const_cast<VkFormatProperties3*>(
|
||||
reinterpret_cast<const VkFormatProperties3*>(FindFeature(
|
||||
properties->pNext, VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3)));
|
||||
extended != nullptr) {
|
||||
extended->linearTilingFeatures = 0;
|
||||
extended->optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(
|
||||
static_cast<VkFormatFeatureFlags>(
|
||||
extended->optimalTilingFeatures));
|
||||
extended->bufferFeatures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties(
|
||||
VkPhysicalDevice physical_device, VkFormat format, VkImageType type,
|
||||
VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags,
|
||||
VkImageFormatProperties* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties>(
|
||||
"vkGetPhysicalDeviceImageFormatProperties", nullptr);
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
return host(physical_device, format, type, tiling, usage, flags,
|
||||
properties);
|
||||
}
|
||||
if (tiling != VK_IMAGE_TILING_OPTIMAL || type != VK_IMAGE_TYPE_2D) {
|
||||
return VK_ERROR_FORMAT_NOT_SUPPORTED;
|
||||
}
|
||||
return host(physical_device, mapped.host_format, type, tiling, usage,
|
||||
flags & ~static_cast<VkImageCreateFlags>(
|
||||
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT),
|
||||
properties);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2(
|
||||
VkPhysicalDevice physical_device,
|
||||
const VkPhysicalDeviceImageFormatInfo2* info,
|
||||
VkImageFormatProperties2* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties2>(
|
||||
"vkGetPhysicalDeviceImageFormatProperties2",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2KHR");
|
||||
if (host == nullptr || info == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(info->format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
return host(physical_device, info, properties);
|
||||
}
|
||||
if (info->tiling != VK_IMAGE_TILING_OPTIMAL ||
|
||||
info->type != VK_IMAGE_TYPE_2D) {
|
||||
return VK_ERROR_FORMAT_NOT_SUPPORTED;
|
||||
}
|
||||
VkPhysicalDeviceImageFormatInfo2 host_info = *info;
|
||||
host_info.format = mapped.host_format;
|
||||
host_info.flags &= ~static_cast<VkImageCreateFlags>(
|
||||
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT);
|
||||
return host(physical_device, &host_info, properties);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
|
||||
VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image) {
|
||||
return State().etc2.CreateImage(device, create_info, allocator, image);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyImage(
|
||||
VkDevice device, VkImage image, const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.DestroyImage(device, image, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
|
||||
VkDevice device, const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImageView* view) {
|
||||
return State().etc2.CreateImageView(device, create_info, allocator, view);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device,
|
||||
VkBuffer buffer,
|
||||
VkDeviceMemory memory,
|
||||
VkDeviceSize offset) {
|
||||
return State().etc2.BindBufferMemory(device, buffer, memory, offset);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2(
|
||||
VkDevice device, std::uint32_t count,
|
||||
const VkBindBufferMemoryInfo* infos) {
|
||||
return State().etc2.BindBufferMemory2(device, count, infos);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device,
|
||||
VkDeviceMemory memory,
|
||||
VkDeviceSize offset,
|
||||
VkDeviceSize size,
|
||||
VkMemoryMapFlags flags,
|
||||
void** data) {
|
||||
return State().etc2.MapMemory(device, memory, offset, size, flags, data);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2(VkDevice device,
|
||||
const VkMemoryMapInfo* info,
|
||||
void** data) {
|
||||
return State().etc2.MapMemory2(device, info, data);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device,
|
||||
VkDeviceMemory memory) {
|
||||
State().etc2.UnmapMemory(device, memory);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2(VkDevice device,
|
||||
const VkMemoryUnmapInfo* info) {
|
||||
return State().etc2.UnmapMemory2(device, info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkFreeMemory(
|
||||
VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.FreeMemory(device, memory, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(
|
||||
VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.DestroyBuffer(device, buffer, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(
|
||||
VkCommandBuffer command_buffer, VkBuffer source, VkImage destination,
|
||||
VkImageLayout layout, std::uint32_t region_count,
|
||||
const VkBufferImageCopy* regions) {
|
||||
State().etc2.CmdCopyBufferToImage(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
source, destination, layout, region_count, regions);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2(
|
||||
VkCommandBuffer command_buffer, const VkCopyBufferToImageInfo2* info) {
|
||||
State().etc2.CmdCopyBufferToImage2(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage(
|
||||
VkCommandBuffer command_buffer, VkImage source,
|
||||
VkImageLayout source_layout, VkImage destination,
|
||||
VkImageLayout destination_layout, std::uint32_t region_count,
|
||||
const VkImageCopy* regions) {
|
||||
State().etc2.CmdCopyImage(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
source, source_layout, destination, destination_layout, region_count,
|
||||
regions);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage2(VkCommandBuffer command_buffer,
|
||||
const VkCopyImageInfo2* info) {
|
||||
State().etc2.CmdCopyImage2(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdExecuteCommands(
|
||||
VkCommandBuffer command_buffer, std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries) {
|
||||
State().etc2.CmdExecuteCommands(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
count, secondaries);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,398 @@
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<int, 8> kDistance = {3, 6, 11, 16, 23, 32, 41, 64};
|
||||
|
||||
// Indexed by (msb << 1) | lsb.
|
||||
constexpr int kEtc1Modifiers[8][4] = {
|
||||
{2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29},
|
||||
{13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80},
|
||||
{33, 106, -33, -106}, {47, 183, -47, -183},
|
||||
};
|
||||
|
||||
constexpr int kEacModifiers[16][8] = {
|
||||
{-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12},
|
||||
{-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12},
|
||||
{-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10},
|
||||
{-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10},
|
||||
{-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9},
|
||||
{-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9},
|
||||
{-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9},
|
||||
{-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8},
|
||||
};
|
||||
|
||||
std::uint8_t Clamp255(int value) {
|
||||
return static_cast<std::uint8_t>(std::clamp(value, 0, 255));
|
||||
}
|
||||
|
||||
int Expand4(int value) { return (value << 4) | value; }
|
||||
int Expand5(int value) { return (value << 3) | (value >> 2); }
|
||||
int Expand6(int value) { return (value << 2) | (value >> 4); }
|
||||
int Expand7(int value) { return (value << 1) | (value >> 6); }
|
||||
|
||||
// Texels use column-major indexing: x * 4 + y.
|
||||
void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
std::array<std::uint8_t, 64>* out) {
|
||||
static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1};
|
||||
const bool diff_or_opaque = (b[3] & 2) != 0;
|
||||
const bool opaque = !punchthrough || diff_or_opaque;
|
||||
const int msb = (b[4] << 8) | b[5];
|
||||
const int lsb = (b[6] << 8) | b[7];
|
||||
auto index_of = [msb, lsb](int texel) {
|
||||
return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1);
|
||||
};
|
||||
auto write = [out](int texel, int r, int g, int bl, int a) {
|
||||
const std::size_t o = static_cast<std::size_t>(texel) * 4;
|
||||
(*out)[o] = Clamp255(r);
|
||||
(*out)[o + 1] = Clamp255(g);
|
||||
(*out)[o + 2] = Clamp255(bl);
|
||||
(*out)[o + 3] = static_cast<std::uint8_t>(a);
|
||||
};
|
||||
|
||||
const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7];
|
||||
const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7];
|
||||
const int b_sum = (b[2] >> 3) + kDelta[b[2] & 7];
|
||||
const bool individual = !punchthrough && !diff_or_opaque;
|
||||
|
||||
if (!individual && (r_sum < 0 || r_sum > 31 || g_sum < 0 || g_sum > 31)) {
|
||||
std::array<std::array<int, 3>, 4> paint{};
|
||||
if (r_sum < 0 || r_sum > 31) {
|
||||
const std::array<int, 3> c1 = {
|
||||
Expand4((((b[0] >> 3) & 3) << 2) | (b[0] & 3)),
|
||||
Expand4(b[1] >> 4), Expand4(b[1] & 15)};
|
||||
const std::array<int, 3> c2 = {Expand4(b[2] >> 4), Expand4(b[2] & 15),
|
||||
Expand4(b[3] >> 4)};
|
||||
const int d = kDistance[(((b[3] >> 2) & 3) << 1) | (b[3] & 1)];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
paint[0][c] = c1[c];
|
||||
paint[1][c] = c2[c] + d;
|
||||
paint[2][c] = c2[c];
|
||||
paint[3][c] = c2[c] - d;
|
||||
}
|
||||
} else {
|
||||
const std::array<int, 3> c1 = {
|
||||
Expand4((b[0] >> 3) & 15),
|
||||
Expand4(((b[0] & 7) << 1) | ((b[1] >> 4) & 1)),
|
||||
Expand4((b[1] & 8) | ((b[1] & 3) << 1) | (b[2] >> 7))};
|
||||
const std::array<int, 3> c2 = {
|
||||
Expand4((b[2] >> 3) & 15), Expand4(((b[2] & 7) << 1) | (b[3] >> 7)),
|
||||
Expand4((b[3] >> 3) & 15)};
|
||||
const int v1 = (c1[0] << 16) | (c1[1] << 8) | c1[2];
|
||||
const int v2 = (c2[0] << 16) | (c2[1] << 8) | c2[2];
|
||||
const int d =
|
||||
kDistance[(b[3] & 4) | ((b[3] & 1) << 1) | (v1 >= v2 ? 1 : 0)];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
paint[0][c] = c1[c] + d;
|
||||
paint[1][c] = c1[c] - d;
|
||||
paint[2][c] = c2[c] + d;
|
||||
paint[3][c] = c2[c] - d;
|
||||
}
|
||||
}
|
||||
for (int texel = 0; texel < 16; ++texel) {
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
} else {
|
||||
write(texel, paint[index][0], paint[index][1], paint[index][2], 255);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!individual && (b_sum < 0 || b_sum > 31)) {
|
||||
const int ro = Expand6((b[0] >> 1) & 63);
|
||||
const int go = Expand7(((b[0] & 1) << 6) | ((b[1] >> 1) & 63));
|
||||
const int bo = Expand6(((b[1] & 1) << 5) | (b[2] & 0x18) |
|
||||
((b[2] & 3) << 1) | (b[3] >> 7));
|
||||
const int rh = Expand6(((b[3] & 0x7c) >> 1) | (b[3] & 1));
|
||||
const int gh = Expand7(b[4] >> 1);
|
||||
const int bh = Expand6(((b[4] & 1) << 5) | (b[5] >> 3));
|
||||
const int rv = Expand6(((b[5] & 7) << 3) | (b[6] >> 5));
|
||||
const int gv = Expand7(((b[6] & 31) << 2) | (b[7] >> 6));
|
||||
const int bv = Expand6(b[7] & 63);
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2,
|
||||
(x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2,
|
||||
(x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<std::array<int, 3>, 2> base{};
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
if (individual) {
|
||||
base[0][c] = Expand4(b[c] >> 4);
|
||||
base[1][c] = Expand4(b[c] & 15);
|
||||
} else {
|
||||
base[0][c] = Expand5(b[c] >> 3);
|
||||
base[1][c] = Expand5((b[c] >> 3) + kDelta[b[c] & 7]);
|
||||
}
|
||||
}
|
||||
const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7};
|
||||
const bool flipped = (b[3] & 1) != 0;
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
const int texel = x * 4 + y;
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0);
|
||||
const int modifier =
|
||||
(!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index];
|
||||
write(texel, base[sub][0] + modifier, base[sub][1] + modifier,
|
||||
base[sub][2] + modifier, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int EacIndex(const std::uint8_t* b, int texel) {
|
||||
std::uint64_t bits = 0;
|
||||
for (int i = 2; i < 8; ++i) {
|
||||
bits = (bits << 8) | b[i];
|
||||
}
|
||||
return static_cast<int>((bits >> (45 - texel * 3)) & 7);
|
||||
}
|
||||
|
||||
int EacModifier(const std::uint8_t* b, int texel) {
|
||||
return kEacModifiers[b[1] & 15][EacIndex(b, texel)];
|
||||
}
|
||||
|
||||
std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) {
|
||||
const int multiplier = b[1] >> 4;
|
||||
const int modifier = EacModifier(b, texel);
|
||||
const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier;
|
||||
if (!is_signed) {
|
||||
const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047);
|
||||
return static_cast<std::uint16_t>((value << 5) | (value >> 6));
|
||||
}
|
||||
const int base = std::max(-127, static_cast<int>(static_cast<std::int8_t>(b[0])));
|
||||
const int value = std::clamp(base * 8 + scaled, -1023, 1023);
|
||||
const int magnitude = value < 0 ? -value : value;
|
||||
const int expanded = (magnitude << 5) | (magnitude >> 5);
|
||||
return static_cast<std::uint16_t>(
|
||||
static_cast<std::int16_t>(value < 0 ? -expanded : expanded));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t EtcBlockBytes(EtcFormat format) {
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed:
|
||||
return 16;
|
||||
default:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t EtcDecodedTexelBytes(EtcFormat format) {
|
||||
switch (format) {
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
return 2;
|
||||
default:
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t kBlocksPerBand = 16384;
|
||||
constexpr std::uint64_t kParallelMinimumBlocks = 65536;
|
||||
|
||||
bool ImageFits(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, const std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
return blocks_wide * blocks_high * EtcBlockBytes(format) <= source_bytes &&
|
||||
static_cast<std::uint64_t>(width) * height *
|
||||
EtcDecodedTexelBytes(format) <=
|
||||
destination_bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
return DecodeEtcImageBlockRows(format, source, source_bytes, width, height, 0,
|
||||
(height + 3) / 4, destination,
|
||||
destination_bytes);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct DecodeBand {
|
||||
EtcDecodeJob* job;
|
||||
std::uint32_t first_block_row;
|
||||
std::uint32_t block_row_count;
|
||||
};
|
||||
|
||||
struct DecodeBandQueue {
|
||||
std::vector<DecodeBand> bands;
|
||||
std::atomic<std::size_t> next{0};
|
||||
|
||||
void Run() {
|
||||
for (;;) {
|
||||
const std::size_t index = next.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= bands.size()) {
|
||||
return;
|
||||
}
|
||||
const DecodeBand& band = bands[index];
|
||||
const EtcDecodeJob& job = *band.job;
|
||||
DecodeEtcImageBlockRows(job.format, job.source, job.source_bytes,
|
||||
job.width, job.height, band.first_block_row,
|
||||
band.block_row_count, job.destination,
|
||||
job.destination_bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void* RunDecodeBandQueue(void* queue) {
|
||||
static_cast<DecodeBandQueue*>(queue)->Run();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count) {
|
||||
if (jobs == nullptr) {
|
||||
return;
|
||||
}
|
||||
DecodeBandQueue queue;
|
||||
std::vector<DecodeBand>& bands = queue.bands;
|
||||
std::uint64_t total_blocks = 0;
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
EtcDecodeJob& job = jobs[index];
|
||||
job.ok = ImageFits(job.format, job.source, job.source_bytes, job.width,
|
||||
job.height, job.destination, job.destination_bytes);
|
||||
if (!job.ok) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t blocks_wide = (job.width + 3) / 4;
|
||||
const std::uint32_t blocks_high = (job.height + 3) / 4;
|
||||
const std::uint32_t rows_per_band = static_cast<std::uint32_t>(
|
||||
std::max<std::uint64_t>(1, kBlocksPerBand / blocks_wide));
|
||||
for (std::uint32_t row = 0; row < blocks_high; row += rows_per_band) {
|
||||
bands.push_back(
|
||||
{&job, row, std::min(rows_per_band, blocks_high - row)});
|
||||
}
|
||||
total_blocks += static_cast<std::uint64_t>(blocks_wide) * blocks_high;
|
||||
}
|
||||
|
||||
const std::size_t threads =
|
||||
total_blocks < kParallelMinimumBlocks
|
||||
? 1
|
||||
: std::min<std::size_t>(std::max(worker_count, 1U), bands.size());
|
||||
std::vector<pthread_t> workers;
|
||||
workers.reserve(threads > 0 ? threads - 1 : 0);
|
||||
for (std::size_t index = 1; index < threads; ++index) {
|
||||
pthread_t worker;
|
||||
if (pthread_create(&worker, nullptr, RunDecodeBandQueue, &queue) != 0) {
|
||||
break;
|
||||
}
|
||||
workers.push_back(worker);
|
||||
}
|
||||
queue.Run();
|
||||
for (pthread_t worker : workers) {
|
||||
pthread_join(worker, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (!ImageFits(format, source, source_bytes, width, height, destination,
|
||||
destination_bytes)) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
if (static_cast<std::uint64_t>(first_block_row) + block_row_count >
|
||||
blocks_high) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t texel_bytes = EtcDecodedTexelBytes(format);
|
||||
const std::size_t block_bytes = EtcBlockBytes(format);
|
||||
|
||||
std::array<std::uint8_t, 64> rgba{};
|
||||
const std::uint64_t end_block_row =
|
||||
static_cast<std::uint64_t>(first_block_row) + block_row_count;
|
||||
for (std::uint64_t by = first_block_row; by < end_block_row; ++by) {
|
||||
for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) {
|
||||
const std::uint8_t* block =
|
||||
source + (by * blocks_wide + bx) * block_bytes;
|
||||
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
|
||||
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba);
|
||||
} else if (format == EtcFormat::kEtc2Rgba8) {
|
||||
DecodeRgbBlock(block + 8, false, &rgba);
|
||||
}
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint8_t* out =
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgb8:
|
||||
case EtcFormat::kEtc2Rgb8A1:
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
std::copy_n(rgba.data() + texel * 4, 4, out);
|
||||
if (format == EtcFormat::kEtc2Rgba8) {
|
||||
const int alpha =
|
||||
block[0] + EacModifier(block, texel) * (block[1] >> 4);
|
||||
out[3] = Clamp255(alpha);
|
||||
}
|
||||
break;
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed: {
|
||||
const bool is_signed = format == EtcFormat::kEacR11Signed ||
|
||||
format == EtcFormat::kEacRg11Signed;
|
||||
const int channels = texel_bytes / 2;
|
||||
for (int channel = 0; channel < channels; ++channel) {
|
||||
const std::uint16_t value =
|
||||
DecodeR11(block + channel * 8, texel, is_signed);
|
||||
out[channel * 2] = static_cast<std::uint8_t>(value);
|
||||
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "mocktail/graphics/texture_override.h"
|
||||
|
||||
#include <png.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size) {
|
||||
std::uint64_t hash = 0xcbf29ce484222325ULL;
|
||||
for (std::size_t index = 0; index < size; ++index) {
|
||||
hash ^= data[index];
|
||||
hash *= 0x100000001b3ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::string HashName(std::uint64_t hash) {
|
||||
char text[17];
|
||||
std::snprintf(text, sizeof(text), "%016llx",
|
||||
static_cast<unsigned long long>(hash));
|
||||
return text;
|
||||
}
|
||||
|
||||
bool ReadPngRgba(const std::string& path, RgbaImage* image) {
|
||||
if (image == nullptr) {
|
||||
return false;
|
||||
}
|
||||
png_image png{};
|
||||
png.version = PNG_IMAGE_VERSION;
|
||||
if (png_image_begin_read_from_file(&png, path.c_str()) == 0) {
|
||||
return false;
|
||||
}
|
||||
png.format = PNG_FORMAT_RGBA;
|
||||
RgbaImage result;
|
||||
result.width = png.width;
|
||||
result.height = png.height;
|
||||
result.pixels.resize(PNG_IMAGE_SIZE(png));
|
||||
if (png_image_finish_read(&png, nullptr, result.pixels.data(), 0,
|
||||
nullptr) == 0) {
|
||||
png_image_free(&png);
|
||||
return false;
|
||||
}
|
||||
*image = std::move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WritePngRgba(const std::string& path, const RgbaImage& image) {
|
||||
if (image.width == 0 || image.height == 0 ||
|
||||
image.pixels.size() !=
|
||||
static_cast<std::size_t>(image.width) * image.height * 4) {
|
||||
return false;
|
||||
}
|
||||
png_image png{};
|
||||
png.version = PNG_IMAGE_VERSION;
|
||||
png.width = image.width;
|
||||
png.height = image.height;
|
||||
png.format = PNG_FORMAT_RGBA;
|
||||
const int written = png_image_write_to_file(&png, path.c_str(), 0,
|
||||
image.pixels.data(), 0, nullptr);
|
||||
png_image_free(&png);
|
||||
return written != 0;
|
||||
}
|
||||
|
||||
void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination) {
|
||||
if (source.pixels.size() <
|
||||
static_cast<std::size_t>(source.width) * source.height * 4) {
|
||||
return;
|
||||
}
|
||||
ResampleRgba(source.pixels.data(), source.width, source.height, width,
|
||||
height, destination);
|
||||
}
|
||||
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0 || source_width == 0 || source_height == 0) {
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t y = 0; y < height; ++y) {
|
||||
const std::uint64_t y0 = static_cast<std::uint64_t>(y) * source_height / height;
|
||||
std::uint64_t y1 =
|
||||
(static_cast<std::uint64_t>(y) + 1) * source_height / height;
|
||||
if (y1 <= y0) {
|
||||
y1 = y0 + 1;
|
||||
}
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const std::uint64_t x0 = static_cast<std::uint64_t>(x) * source_width / width;
|
||||
std::uint64_t x1 =
|
||||
(static_cast<std::uint64_t>(x) + 1) * source_width / width;
|
||||
if (x1 <= x0) {
|
||||
x1 = x0 + 1;
|
||||
}
|
||||
std::uint64_t sum[4] = {0, 0, 0, 0};
|
||||
for (std::uint64_t sy = y0; sy < y1; ++sy) {
|
||||
for (std::uint64_t sx = x0; sx < x1; ++sx) {
|
||||
const std::uint8_t* pixel = source + (sy * source_width + sx) * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
sum[channel] += pixel[channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::uint64_t count = (y1 - y0) * (x1 - x0);
|
||||
std::uint8_t* out = destination + (static_cast<std::size_t>(y) * width + x) * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out[channel] =
|
||||
static_cast<std::uint8_t>((sum[channel] + count / 2) / count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string EnvironmentString(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr ? value : "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextureOverrides::TextureOverrides(std::string override_dir,
|
||||
std::string dump_dir)
|
||||
: override_dir_(std::move(override_dir)), dump_dir_(std::move(dump_dir)) {}
|
||||
|
||||
TextureOverrides TextureOverrides::FromEnvironment() {
|
||||
return TextureOverrides(EnvironmentString("MOCKTAIL_TEXTURE_OVERRIDE_DIR"),
|
||||
EnvironmentString("MOCKTAIL_TEXTURE_DUMP_DIR"));
|
||||
}
|
||||
|
||||
std::shared_ptr<const RgbaImage> TextureOverrides::Lookup(std::uint64_t hash) {
|
||||
if (override_dir_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto cached = cache_.find(hash);
|
||||
if (cached != cache_.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
std::shared_ptr<const RgbaImage> loaded;
|
||||
RgbaImage image;
|
||||
const std::string path = override_dir_ + "/" + HashName(hash) + ".png";
|
||||
if (ReadPngRgba(path, &image) && image.width > 0 && image.height > 0) {
|
||||
loaded = std::make_shared<const RgbaImage>(std::move(image));
|
||||
std::fprintf(stderr, " [vulkan] texture override loaded: %s (%ux%u)\n",
|
||||
path.c_str(), loaded->width, loaded->height);
|
||||
}
|
||||
cache_[hash] = loaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void TextureOverrides::Dump(std::uint64_t hash, std::uint32_t width,
|
||||
std::uint32_t height, const std::uint8_t* rgba) {
|
||||
if (dump_dir_.empty() || rgba == nullptr || width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!dumped_.insert(hash).second) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(dump_dir_, error);
|
||||
RgbaImage image;
|
||||
image.width = width;
|
||||
image.height = height;
|
||||
image.pixels.assign(rgba, rgba + static_cast<std::size_t>(width) * height * 4);
|
||||
const std::string path = dump_dir_ + "/" + HashName(hash) + "_" +
|
||||
std::to_string(width) + "x" +
|
||||
std::to_string(height) + ".png";
|
||||
if (!WritePngRgba(path, image)) {
|
||||
std::fprintf(stderr, " [vulkan] texture dump failed: %s\n", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
File diff suppressed because it is too large.
Load diff
+50
-22
@@ -23,6 +23,8 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/display_size.h"
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
void Class::RegisterMethod(const std::string& method_name,
|
||||
@@ -562,13 +564,25 @@ bool ResolvedDarkTheme() {
|
||||
}
|
||||
|
||||
jint IntResultForName(const char* name) {
|
||||
if (std::strcmp(name, "getScreenWidth") == 0 ||
|
||||
std::strcmp(name, "getWidth") == 0) {
|
||||
return 1280;
|
||||
if (std::strcmp(name, "getScreenWidth") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment))
|
||||
.width;
|
||||
}
|
||||
if (std::strcmp(name, "getScreenHeight") == 0 ||
|
||||
std::strcmp(name, "getHeight") == 0) {
|
||||
return 720;
|
||||
if (std::strcmp(name, "getScreenHeight") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment))
|
||||
.height;
|
||||
}
|
||||
if (std::strcmp(name, "getWidth") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment))
|
||||
.width;
|
||||
}
|
||||
if (std::strcmp(name, "getHeight") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment))
|
||||
.height;
|
||||
}
|
||||
if (std::strcmp(name, "getDensityDpi") == 0) {
|
||||
return 160;
|
||||
@@ -2275,8 +2289,11 @@ jobject MakeDeviceStaticParamsObject() {
|
||||
pseudo_object->object_fields["soc_model"] =
|
||||
MakeString(identity.soc_model.c_str());
|
||||
pseudo_object->boolean_fields["cpu64Bit"] = JNI_TRUE;
|
||||
pseudo_object->int_fields["screenWidth"] = 1280;
|
||||
pseudo_object->int_fields["screenHeight"] = 720;
|
||||
const mocktail::runtime::DisplaySize host_display =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
pseudo_object->int_fields["screenWidth"] = host_display.width;
|
||||
pseudo_object->int_fields["screenHeight"] = host_display.height;
|
||||
pseudo_object->int_fields["screenDensityDpi"] = 160;
|
||||
pseudo_object->int_fields["apiVersion"] = 33;
|
||||
pseudo_object->int_fields["sdkVersion"] = 33;
|
||||
@@ -2446,14 +2463,17 @@ void EnsureAndroidObjectGraph() {
|
||||
SetObjectFieldRaw(decor_view, "holder", surface_holder);
|
||||
SetObjectFieldRaw(decor_view, "surface", surface);
|
||||
SetObjectFieldRaw(decor_view, "display", display);
|
||||
SetIntFieldRaw(decor_view, "width", 1280);
|
||||
SetIntFieldRaw(decor_view, "height", 720);
|
||||
const mocktail::runtime::DisplaySize host_window =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
SetIntFieldRaw(decor_view, "width", host_window.width);
|
||||
SetIntFieldRaw(decor_view, "height", host_window.height);
|
||||
|
||||
SetObjectFieldRaw(surface_view, "holder", surface_holder);
|
||||
SetObjectFieldRaw(surface_view, "surface", surface);
|
||||
SetObjectFieldRaw(surface_view, "rootView", root_view);
|
||||
SetIntFieldRaw(surface_view, "width", 1280);
|
||||
SetIntFieldRaw(surface_view, "height", 720);
|
||||
SetIntFieldRaw(surface_view, "width", host_window.width);
|
||||
SetIntFieldRaw(surface_view, "height", host_window.height);
|
||||
|
||||
SetObjectFieldRaw(surface_holder, "surface", surface);
|
||||
SetObjectFieldRaw(surface_holder, "surfaceFrame",
|
||||
@@ -2461,11 +2481,14 @@ void EnsureAndroidObjectGraph() {
|
||||
SetBooleanFieldRaw(surface, "valid", JNI_TRUE);
|
||||
SetBooleanFieldRaw(surface, "isValid", JNI_TRUE);
|
||||
|
||||
SetIntFieldRaw(display, "width", 1280);
|
||||
SetIntFieldRaw(display, "height", 720);
|
||||
const mocktail::runtime::DisplaySize host_display =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
SetIntFieldRaw(display, "width", host_display.width);
|
||||
SetIntFieldRaw(display, "height", host_display.height);
|
||||
SetIntFieldRaw(display, "rotation", 0);
|
||||
SetIntFieldRaw(display_metrics, "widthPixels", 1280);
|
||||
SetIntFieldRaw(display_metrics, "heightPixels", 720);
|
||||
SetIntFieldRaw(display_metrics, "widthPixels", host_display.width);
|
||||
SetIntFieldRaw(display_metrics, "heightPixels", host_display.height);
|
||||
SetIntFieldRaw(display_metrics, "densityDpi", 160);
|
||||
SetFloatFieldRaw(display_metrics, "density", 1.0f);
|
||||
SetFloatFieldRaw(display_metrics, "scaledDensity", 1.0f);
|
||||
@@ -2476,9 +2499,10 @@ void EnsureAndroidObjectGraph() {
|
||||
: PlatformIdentity{};
|
||||
SetIntFieldRaw(configuration, "orientation", 2);
|
||||
SetIntFieldRaw(configuration, "densityDpi", 160);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", 1280);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", 720);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", host_window.width);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", host_window.height);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp",
|
||||
std::min(host_window.width, host_window.height));
|
||||
SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1);
|
||||
SetIntFieldRaw(configuration, "keyboard", identity.keyboard_enabled ? 2 : 1);
|
||||
SetIntFieldRaw(configuration, "keyboardHidden",
|
||||
@@ -4952,10 +4976,14 @@ jobject CreateAndroidConfiguration(JNIEnv* env) {
|
||||
SetIntFieldRaw(configuration, "navigation", 1);
|
||||
SetIntFieldRaw(configuration, "navigationHidden", 1);
|
||||
SetIntFieldRaw(configuration, "orientation", 2);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", 720);
|
||||
const mocktail::runtime::DisplaySize config_window =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", config_window.height);
|
||||
SetIntFieldRaw(configuration, "screenLayout", 0);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", 1280);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", config_window.width);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp",
|
||||
std::min(config_window.width, config_window.height));
|
||||
SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1);
|
||||
SetIntFieldRaw(configuration, "uiMode", 0);
|
||||
return configuration;
|
||||
|
||||
+157
-35
@@ -3,6 +3,7 @@
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -35,7 +36,9 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/prctl.h>
|
||||
#if defined(__x86_64__)
|
||||
#include <asm/prctl.h>
|
||||
#endif
|
||||
#include <sys/syscall.h>
|
||||
#include <vector>
|
||||
|
||||
@@ -48,6 +51,7 @@
|
||||
#include "compat/bionic_socket_runtime.h"
|
||||
#include "compat/build_profile.h"
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "compat/host_abi_experiment.h"
|
||||
#include "compat/host_abi_profile.h"
|
||||
#include "compat/host_allocator_bridge.h"
|
||||
@@ -56,6 +60,8 @@
|
||||
#include "libc_shim/libc_shim.h"
|
||||
#include "linker/linker.h"
|
||||
#include "mocktail/graphics/bionic_egl_bridge.h"
|
||||
#include "runtime/device_memory_profile.h"
|
||||
#include "runtime/display_size.h"
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/discord_rpc.h"
|
||||
#include "runtime/jnivm_platform_web_callbacks.h"
|
||||
@@ -70,6 +76,7 @@
|
||||
#include "runtime/roblox_text_input_jni_bridge.h"
|
||||
#include "runtime/runtime_config.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
#include "runtime/texture_memory_policy.h"
|
||||
#include "services/client_settings_service.h"
|
||||
#include "services/http_client.h"
|
||||
#include "window/window.h"
|
||||
@@ -1002,7 +1009,7 @@ struct BionicAddrInfo {
|
||||
};
|
||||
|
||||
static_assert(sizeof(BionicAddrInfo) == 48,
|
||||
"unexpected x86_64 bionic addrinfo size");
|
||||
"unexpected bionic addrinfo size");
|
||||
|
||||
bool DnsTraceEnabled() {
|
||||
return IsEnabled("MOCKTAIL_DNS_TRACE");
|
||||
@@ -1367,15 +1374,31 @@ bool IsUnsafeSoftTimeoutModule(void* rip) {
|
||||
void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) {
|
||||
static_cast<void>(info);
|
||||
auto* uc = static_cast<ucontext_t*>(context);
|
||||
#if defined(__x86_64__)
|
||||
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]) : 0;
|
||||
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RSP]) : 0;
|
||||
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]) : 0;
|
||||
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RAX]) : 0;
|
||||
const char* regs_format =
|
||||
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
|
||||
"RAX=0x%016llx\n";
|
||||
#elif defined(__aarch64__)
|
||||
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.pc) : 0;
|
||||
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.sp) : 0;
|
||||
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[29]) : 0;
|
||||
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[0]) : 0;
|
||||
const char* regs_format =
|
||||
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
|
||||
"X0=0x%016llx\n";
|
||||
#else
|
||||
uintptr_t rip = 0, rsp = 0, rbp = 0, rax = 0;
|
||||
const char* regs_format =
|
||||
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
|
||||
"X0=0x%016llx\n";
|
||||
#endif
|
||||
char regs_msg[192];
|
||||
int len = std::snprintf(
|
||||
regs_msg, sizeof(regs_msg),
|
||||
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
|
||||
"RAX=0x%016llx\n",
|
||||
regs_msg, sizeof(regs_msg), regs_format,
|
||||
static_cast<unsigned long long>(rip), static_cast<unsigned long long>(rsp),
|
||||
static_cast<unsigned long long>(rbp), static_cast<unsigned long long>(rax));
|
||||
write(2, regs_msg, static_cast<size_t>(len));
|
||||
@@ -2393,6 +2416,21 @@ void InstallNativeGlJavaImplementation(JNIEnv* env, jclass native_gl_java_class)
|
||||
|
||||
jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless);
|
||||
|
||||
mocktail::runtime::DisplaySize HostDisplaySize() {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
}
|
||||
|
||||
mocktail::runtime::DisplaySize HostWindowPixelSize() {
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
if (viewport.valid()) {
|
||||
return {viewport.pixel_width, viewport.pixel_height};
|
||||
}
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
}
|
||||
|
||||
jobject BuildDeviceParams(JNIEnv* env) {
|
||||
jobject params = NewObject(env, "com/roblox/engine/jni/model/DeviceParams");
|
||||
if (!params) {
|
||||
@@ -2403,10 +2441,12 @@ jobject BuildDeviceParams(JNIEnv* env) {
|
||||
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux");
|
||||
const std::string manufacturer =
|
||||
GetEnvString("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
|
||||
const std::string device_sku =
|
||||
GetEnvString("MOCKTAIL_DEVICE_SKU", "mocktail-x86_64");
|
||||
const std::string device_sku = GetEnvString(
|
||||
"MOCKTAIL_DEVICE_SKU",
|
||||
("mocktail-" + std::string(mocktail::compat::kGuestCpuName)).c_str());
|
||||
const std::string soc_model =
|
||||
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL", "x86_64");
|
||||
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL",
|
||||
std::string(mocktail::compat::kGuestCpuName).c_str());
|
||||
SetStringField(env, params, "osVersion", "33");
|
||||
SetStringField(env, params, "deviceName", device_name.c_str());
|
||||
const std::string app_version =
|
||||
@@ -2414,27 +2454,40 @@ jobject BuildDeviceParams(JNIEnv* env) {
|
||||
SetStringField(env, params, "appVersion", app_version.c_str());
|
||||
SetStringField(env, params, "country", "US");
|
||||
SetStringField(env, params, "manufacturer", manufacturer.c_str());
|
||||
SetStringField(env, params, "displayResolution", "1280x720");
|
||||
const mocktail::runtime::DisplaySize display_size =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
const std::string display_resolution = std::to_string(display_size.width) +
|
||||
"x" +
|
||||
std::to_string(display_size.height);
|
||||
SetStringField(env, params, "displayResolution", display_resolution.c_str());
|
||||
SetStringField(env, params, "networkType", "wifi");
|
||||
SetStringField(env, params, "deviceSku", device_sku.c_str());
|
||||
SetStringField(env, params, "socModel", soc_model.c_str());
|
||||
SetStringField(env, params, "appBuildVariant", "googleProdRelease");
|
||||
SetStringField(env, params, "testDeviceName", device_name.c_str());
|
||||
|
||||
const bool is_low_ram = !IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE");
|
||||
SetIntField(env, params, "deviceTotalMemoryMB", is_low_ram ? 2048 : 4096);
|
||||
SetIntField(env, params, "displayPhysicalWidthPixels", 1280);
|
||||
SetIntField(env, params, "displayPhysicalHeightPixels", 720);
|
||||
SetIntField(env, params, "memoryClass", is_low_ram ? 256 : 512);
|
||||
SetIntField(env, params, "largeMemoryClass", is_low_ram ? 512 : 1024);
|
||||
std::uint64_t host_memory_bytes = mocktail::runtime::DetectHostMemoryBytes();
|
||||
if (IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE")) {
|
||||
host_memory_bytes =
|
||||
std::max<std::uint64_t>(host_memory_bytes, 4096ULL * 1024ULL * 1024ULL);
|
||||
}
|
||||
const mocktail::runtime::DeviceMemoryProfile memory =
|
||||
mocktail::runtime::BuildDeviceMemoryProfile(host_memory_bytes);
|
||||
SetIntField(env, params, "deviceTotalMemoryMB", memory.total_memory_mb);
|
||||
SetIntField(env, params, "displayPhysicalWidthPixels", display_size.width);
|
||||
SetIntField(env, params, "displayPhysicalHeightPixels",
|
||||
display_size.height);
|
||||
SetIntField(env, params, "memoryClass", memory.memory_class_mb);
|
||||
SetIntField(env, params, "largeMemoryClass", memory.large_memory_class_mb);
|
||||
SetLongField(env, params, "lowMemoryKillerBackgroundAppThreshold",
|
||||
is_low_ram ? 256 : 0);
|
||||
memory.low_memory_killer_background_threshold);
|
||||
SetLongField(env, params, "lowMemoryKillerForegroundAppThreshold",
|
||||
is_low_ram ? 512 : 0);
|
||||
memory.low_memory_killer_foreground_threshold);
|
||||
SetBooleanField(env, params, "cpu64Bit", JNI_TRUE);
|
||||
SetBooleanField(env, params, "isChrome", JNI_FALSE);
|
||||
SetBooleanField(env, params, "isLowRamDevice",
|
||||
is_low_ram ? JNI_TRUE : JNI_FALSE);
|
||||
memory.low_ram_device ? JNI_TRUE : JNI_FALSE);
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -2534,8 +2587,9 @@ jobject BuildAppBridgeInitParams(JNIEnv* env, jstring client_settings,
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetRobloxServiceUrlFields(env, params);
|
||||
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize init_display = HostDisplaySize();
|
||||
SetIntField(env, params, "screenWidth", init_display.width);
|
||||
SetIntField(env, params, "screenHeight", init_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "sdkVersion", 33);
|
||||
jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE;
|
||||
@@ -2569,13 +2623,19 @@ jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless) {
|
||||
SetStringField(env, params, "deviceName", platform_device_name.c_str());
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetStringField(env, params, "assetFolderPath", DefaultAssetPath().c_str());
|
||||
SetIntField(env, params, "width", 1280);
|
||||
SetIntField(env, params, "height", 720);
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize platform_window = HostWindowPixelSize();
|
||||
const mocktail::runtime::DisplaySize platform_display = HostDisplaySize();
|
||||
SetIntField(env, params, "width", platform_window.width);
|
||||
SetIntField(env, params, "height", platform_window.height);
|
||||
SetIntField(env, params, "screenWidth", platform_display.width);
|
||||
SetIntField(env, params, "screenHeight", platform_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "viewportWidthMm", 203);
|
||||
SetIntField(env, params, "viewportHeightMm", 114);
|
||||
SetIntField(env, params, "viewportWidthMm",
|
||||
mocktail::runtime::PixelsToMillimetersAt160Dpi(
|
||||
platform_window.width));
|
||||
SetIntField(env, params, "viewportHeightMm",
|
||||
mocktail::runtime::PixelsToMillimetersAt160Dpi(
|
||||
platform_window.height));
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
SetFloatField(env, params, "dpiScale", viewport.dpi_scale);
|
||||
@@ -2684,10 +2744,12 @@ jobject BuildStartAppParams(JNIEnv* env, jstring app_params,
|
||||
SetStringField(env, params, "deviceName", start_device_name.c_str());
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetRobloxServiceUrlFields(env, params);
|
||||
SetIntField(env, params, "width", 1280);
|
||||
SetIntField(env, params, "height", 720);
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize start_window = HostWindowPixelSize();
|
||||
const mocktail::runtime::DisplaySize start_display = HostDisplaySize();
|
||||
SetIntField(env, params, "width", start_window.width);
|
||||
SetIntField(env, params, "height", start_window.height);
|
||||
SetIntField(env, params, "screenWidth", start_display.width);
|
||||
SetIntField(env, params, "screenHeight", start_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "sdkVersion", 33);
|
||||
SetIntField(env, params, "placeId", 0);
|
||||
@@ -2815,8 +2877,9 @@ jobject BuildMockSurface(JNIEnv* env) {
|
||||
if (!surface) {
|
||||
return nullptr;
|
||||
}
|
||||
SetIntField(env, surface, "width", 1280);
|
||||
SetIntField(env, surface, "height", 720);
|
||||
const mocktail::runtime::DisplaySize surface_size = HostWindowPixelSize();
|
||||
SetIntField(env, surface, "width", surface_size.width);
|
||||
SetIntField(env, surface, "height", surface_size.height);
|
||||
SetBooleanField(env, surface, "valid", JNI_TRUE);
|
||||
SetBooleanField(env, surface, "isValid", JNI_TRUE);
|
||||
return surface;
|
||||
@@ -3913,7 +3976,10 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n"
|
||||
on_surface_created(env, game_activity, handle, surface);
|
||||
}
|
||||
if (run_lifecycle_callbacks && on_surface_changed) {
|
||||
on_surface_changed(env, game_activity, handle, surface, 4, 1280, 720);
|
||||
const mocktail::runtime::DisplaySize changed_size =
|
||||
HostWindowPixelSize();
|
||||
on_surface_changed(env, game_activity, handle, surface, 4,
|
||||
changed_size.width, changed_size.height);
|
||||
}
|
||||
if (run_lifecycle_callbacks && on_surface_redraw_needed) {
|
||||
on_surface_redraw_needed(env, game_activity, handle, surface);
|
||||
@@ -4084,8 +4150,9 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n"
|
||||
ShouldRunStartupStep("MOCKTAIL_UPDATE_APP_UI_SIZES", false)) {
|
||||
env = ensure_env();
|
||||
std::cout << " [engine] updateAppUISizes\n" << std::flush;
|
||||
context->native_update_app_ui_sizes(env, native_gl_class, 1280, 720, 0, 0,
|
||||
0);
|
||||
const mocktail::runtime::DisplaySize ui_size = HostWindowPixelSize();
|
||||
context->native_update_app_ui_sizes(env, native_gl_class, ui_size.width,
|
||||
ui_size.height, 0, 0, 0);
|
||||
std::cout << " [engine] updateAppUISizes returned\n" << std::flush;
|
||||
}
|
||||
|
||||
@@ -4575,12 +4642,14 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
|
||||
// return mismatches. Unsupported kernels ignore this request.
|
||||
#if defined(__x86_64__)
|
||||
{
|
||||
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
|
||||
if (r == 0) {
|
||||
std::cout << " [cet] shadow-stack (SHSTK) disabled\n";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (build_profile.allow_legacy_binary_patches) {
|
||||
std::cerr
|
||||
@@ -4726,6 +4795,37 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
if (window_initialised) {
|
||||
auto* sdl_window =
|
||||
static_cast<SDL_Window*>(mocktail::window::GetBackendWindow());
|
||||
const SDL_DisplayID display =
|
||||
sdl_window != nullptr ? SDL_GetDisplayForWindow(sdl_window) : 0;
|
||||
const SDL_DisplayMode* mode =
|
||||
display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr;
|
||||
if (mode != nullptr && mode->w > 0 && mode->h > 0) {
|
||||
// SDL mode dimensions need pixel_density to become physical pixels.
|
||||
const float density = mode->pixel_density > 0.0f ? mode->pixel_density
|
||||
: 1.0f;
|
||||
const std::string display_size =
|
||||
std::to_string(std::lround(mode->w * density)) + "x" +
|
||||
std::to_string(std::lround(mode->h * density));
|
||||
setenv(mocktail::runtime::kDisplaySizeEnvironment, display_size.c_str(),
|
||||
1);
|
||||
std::cout << " [window] host display " << display_size << " pixels\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
if (window_initialised) {
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
if (viewport.valid()) {
|
||||
const std::string window_size = std::to_string(viewport.pixel_width) +
|
||||
"x" +
|
||||
std::to_string(viewport.pixel_height);
|
||||
setenv(mocktail::runtime::kWindowSizeEnvironment, window_size.c_str(),
|
||||
1);
|
||||
}
|
||||
}
|
||||
const SDL_SystemTheme system_theme =
|
||||
window_initialised ? SDL_GetSystemTheme() : SDL_SYSTEM_THEME_UNKNOWN;
|
||||
const bool system_dark_theme = system_theme == SDL_SYSTEM_THEME_DARK;
|
||||
@@ -5468,6 +5568,27 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
reinterpret_cast<void*>(mocktail_pthread_spin_trylock));
|
||||
linker::RegisterSymbol("pthread_spin_unlock",
|
||||
reinterpret_cast<void*>(mocktail_pthread_spin_unlock));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_init",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_init));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_destroy",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_destroy));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setstacksize",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setstacksize));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setdetachstate",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setdetachstate));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setschedparam",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setschedparam));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_getattr_np",
|
||||
reinterpret_cast<void*>(mocktail_pthread_getattr_np));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_getstack",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_getstack));
|
||||
linker::RegisterSymbol("pthread_barrier_init",
|
||||
reinterpret_cast<void*>(mocktail_pthread_barrier_init));
|
||||
linker::RegisterSymbol(
|
||||
@@ -5587,7 +5708,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
if (roblox_handle == nullptr) {
|
||||
std::cerr << "\n[FATAL] Could not load '" << library_path << "'.\n"
|
||||
<< " Extract lib/x86_64/libroblox.so from a Roblox APK and\n"
|
||||
<< " Extract lib/" << compat::kGuestAbi
|
||||
<< "/libroblox.so from a Roblox APK and\n"
|
||||
<< " place it at the path above (or set ROBLOX_LIB_PATH).\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "mocktail/platform/sdl_event_converter.h"
|
||||
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
@@ -71,14 +69,11 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
|
||||
source.button.x, source.button.y};
|
||||
return true;
|
||||
case SDL_EVENT_MOUSE_WHEEL: {
|
||||
float delta_x = source.wheel.x;
|
||||
float delta_y = source.wheel.y;
|
||||
if (source.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
delta_x = -delta_x;
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
// SDL already applies the host's natural-scrolling preference to x/y.
|
||||
// Undoing SDL_MOUSEWHEEL_FLIPPED here would ignore the OS setting.
|
||||
destination->payload = MouseWheelEvent{
|
||||
delta_x, delta_y, source.wheel.mouse_x, source.wheel.mouse_y};
|
||||
source.wheel.x, source.wheel.y, source.wheel.mouse_x,
|
||||
source.wheel.mouse_y};
|
||||
return true;
|
||||
}
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
#include "runtime/roblox_launch_uri.h"
|
||||
|
||||
namespace mocktail {
|
||||
@@ -241,11 +242,12 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
usage
|
||||
<< "Usage: " << (program_name.empty() ? "mocktail" : program_name)
|
||||
<< " [options] [roblox-uri]\n\n"
|
||||
<< "Normal startup downloads and activates a verified managed x86_64 "
|
||||
"Roblox payload automatically.\n\n"
|
||||
<< "Normal startup downloads and activates a verified managed "
|
||||
<< compat::kGuestAbi
|
||||
<< " Roblox payload automatically.\n\n"
|
||||
<< "Options:\n"
|
||||
<< " --roblox-lib <path> Development override for a specific "
|
||||
"x86_64 libroblox.so\n"
|
||||
<< compat::kGuestAbi << " libroblox.so\n"
|
||||
<< " --headless Run without creating an SDL window\n"
|
||||
<< " --windowed Force windowed startup (default)\n"
|
||||
<< " --graphics <backend> direct-vulkan | opengl | system | "
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "runtime/device_memory_profile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t kMebibyte = 1024U * 1024U;
|
||||
constexpr std::uint64_t kMinimumNormalRamBytes = 4096U * kMebibyte;
|
||||
|
||||
} // namespace
|
||||
|
||||
DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes) {
|
||||
DeviceMemoryProfile profile;
|
||||
if (host_memory_bytes < kMinimumNormalRamBytes) {
|
||||
profile.total_memory_mb = 2048;
|
||||
profile.memory_class_mb = 256;
|
||||
profile.large_memory_class_mb = 512;
|
||||
profile.low_memory_killer_background_threshold = 256;
|
||||
profile.low_memory_killer_foreground_threshold = 512;
|
||||
profile.low_ram_device = true;
|
||||
return profile;
|
||||
}
|
||||
profile.total_memory_mb = static_cast<std::int32_t>(std::min<std::uint64_t>(
|
||||
host_memory_bytes / kMebibyte,
|
||||
static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max())));
|
||||
profile.memory_class_mb = 512;
|
||||
profile.large_memory_class_mb = 1024;
|
||||
return profile;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -18,8 +18,6 @@ namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
// Keep the Vulkan shader workaround narrowly scoped. TextureManager2 is left
|
||||
// enabled so the runtime can use its normal texture path and mip selection.
|
||||
constexpr char kVulkanClientSettingsOverrides[] =
|
||||
R"({"FStringGraphicsVulkanShaderMTDenyPattern":"4318:.*"})";
|
||||
|
||||
@@ -29,12 +27,21 @@ constexpr const char* kIcdDirectories[] = {
|
||||
"/usr/local/share/vulkan/icd.d",
|
||||
};
|
||||
|
||||
#if defined(__aarch64__)
|
||||
constexpr char kNativeArchitecture[] = "aarch64";
|
||||
|
||||
constexpr const char* kForeignArchitectures[] = {
|
||||
"i686", "i586", "i486", "i386", "x86_64", "x86.",
|
||||
"amd64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
|
||||
};
|
||||
#else
|
||||
constexpr char kNativeArchitecture[] = "x86_64";
|
||||
|
||||
constexpr const char* kForeignArchitectures[] = {
|
||||
"i686", "i586", "i486", "i386", "x86.", "aarch64",
|
||||
"arm64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
|
||||
};
|
||||
#endif
|
||||
|
||||
struct HostGpus {
|
||||
bool intel = false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
#include "runtime/external_launch_broker.h"
|
||||
#include "runtime/roblox_launch_uri.h"
|
||||
#include "runtime/webview_helper_launcher.h"
|
||||
#include "window/window.h"
|
||||
|
||||
@@ -951,9 +952,23 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
|
||||
route = web_surface_route_;
|
||||
web_view_bridge = web_view_bridge_.get();
|
||||
}
|
||||
return event.type == WebViewHelperEventType::kRobloxCookie
|
||||
? AcceptWebViewRobloxCookie(event.payload)
|
||||
: RouteWebSurfaceEvent(route, event, web_view_bridge);
|
||||
if (event.type == WebViewHelperEventType::kRobloxCookie) {
|
||||
return AcceptWebViewRobloxCookie(event.payload);
|
||||
}
|
||||
if (event.type == WebViewHelperEventType::kExecuteRoblox) {
|
||||
// Roblox defers a signalled launchGame until the Lua app next navigates.
|
||||
// A parsed launchGame is launched here and never forwarded, so a deferred
|
||||
// copy cannot start a second join later.
|
||||
RobloxExperienceLaunchRequest request;
|
||||
if (ParseRobloxWebViewLaunchGame(event.payload, &request).ok() &&
|
||||
Dispatch(request).ok()) {
|
||||
std::fprintf(stderr,
|
||||
"[experience] WebView launchGame queued place_id=%lld\n",
|
||||
static_cast<long long>(request.place_id));
|
||||
return CloseWebSurface();
|
||||
}
|
||||
}
|
||||
return RouteWebSurfaceEvent(route, event, web_view_bridge);
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::DrainPlatformEvents() {
|
||||
|
||||
@@ -640,5 +640,64 @@ Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
return MakeRequest(fields, request);
|
||||
}
|
||||
|
||||
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||
RobloxExperienceLaunchRequest* request) {
|
||||
if (request == nullptr) {
|
||||
return Invalid("Roblox launchGame output is null");
|
||||
}
|
||||
*request = {};
|
||||
const nlohmann::json document =
|
||||
command.size() <= kMaximumRobloxLaunchUriBytes
|
||||
? nlohmann::json::parse(command, nullptr, false)
|
||||
: nlohmann::json();
|
||||
const auto function_name = document.is_object()
|
||||
? document.find("functionName")
|
||||
: document.end();
|
||||
if (!document.is_object() || function_name == document.end() ||
|
||||
*function_name != "launchGame") {
|
||||
return Status::Error(StatusCode::kUnsupported,
|
||||
"WebView command is not launchGame");
|
||||
}
|
||||
const auto params = document.find("params");
|
||||
if (params == document.end() || !params->is_object()) {
|
||||
return Invalid("Roblox launchGame has no params");
|
||||
}
|
||||
const auto launch = params->find("request");
|
||||
if (launch == params->end() || !launch->is_object()) {
|
||||
return Invalid("Roblox launchGame has no request");
|
||||
}
|
||||
if (launch->size() > kMaximumParameterCount) {
|
||||
return Invalid("Roblox launchGame request has too many fields");
|
||||
}
|
||||
LaunchFields fields;
|
||||
for (const auto& [key, value] : launch->items()) {
|
||||
std::string text;
|
||||
if (value.is_string()) {
|
||||
text = value.get<std::string>();
|
||||
} else if (value.is_number_unsigned()) {
|
||||
text = std::to_string(value.get<uint64_t>());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (text.size() > kMaximumParameterValueBytes ||
|
||||
ContainsControlBytes(text)) {
|
||||
return Invalid("Roblox launchGame field is invalid");
|
||||
}
|
||||
Status status = AssignField(key == "requestType" ? "request" : key,
|
||||
std::move(text), false, &fields);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
if (fields.request_type.empty()) {
|
||||
return Invalid("Roblox launchGame request type is missing");
|
||||
}
|
||||
Status status = MakeRequest(fields, request);
|
||||
if (!status.ok()) {
|
||||
*request = {};
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -1,3 +1,4 @@
|
||||
// Includes changes by vii from CoderDayton/nightcap.
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
#define JSON_NOEXCEPTION 1
|
||||
@@ -13,6 +14,8 @@
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
@@ -437,8 +440,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
const std::filesystem::path library = canonical_root / "libroblox.so";
|
||||
const std::filesystem::path package_root = canonical_root / "sober_apk";
|
||||
const std::filesystem::path base_apk = package_root / "base.apk";
|
||||
const std::filesystem::path x86_64_split_apk =
|
||||
package_root / "split_config.x86_64.apk";
|
||||
const std::filesystem::path arch_split_apk =
|
||||
package_root / compat::kGuestSplitApkFile;
|
||||
const std::filesystem::path assets_root = canonical_root / "assets";
|
||||
const std::filesystem::path assets = canonical_root / "assets/content";
|
||||
const std::filesystem::file_status library_status =
|
||||
@@ -459,7 +462,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
for (const auto& [path, name] :
|
||||
std::array<std::pair<std::filesystem::path, const char*>, 2>{
|
||||
std::pair{base_apk, "base.apk"},
|
||||
std::pair{x86_64_split_apk, "split_config.x86_64.apk"}}) {
|
||||
std::pair{arch_split_apk,
|
||||
compat::kGuestSplitApkFile.data()}}) {
|
||||
const std::filesystem::file_status status =
|
||||
std::filesystem::symlink_status(path, filesystem_error);
|
||||
if (filesystem_error || !std::filesystem::is_regular_file(status) ||
|
||||
@@ -488,7 +492,7 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
result.root = canonical_root;
|
||||
result.roblox_library = library;
|
||||
result.base_apk = base_apk;
|
||||
result.x86_64_split_apk = x86_64_split_apk;
|
||||
result.arch_split_apk = arch_split_apk;
|
||||
result.assets_content = assets;
|
||||
|
||||
const auto compatibility_manifest =
|
||||
@@ -603,7 +607,9 @@ bool ExportRuntimePathEnvironment(const RuntimePaths& paths,
|
||||
SetEnvironmentDefault("MOCKTAIL_ANDROID_CACHE_HOST_ROOT",
|
||||
paths.android_cache_root(), error) &&
|
||||
SetEnvironmentDefault("MOCKTAIL_VULKAN_SHADER_CACHE_HOST_PATH",
|
||||
paths.vulkan_shader_cache_file(), error);
|
||||
paths.vulkan_shader_cache_file(), error) &&
|
||||
SetEnvironmentDefault("MOCKTAIL_TEXTURE_OVERRIDE_DIR",
|
||||
paths.config_root() / "textures", error);
|
||||
}
|
||||
|
||||
bool PrepareManagedPayloadWorkingDirectory(const RuntimePaths& paths,
|
||||
|
||||
+25
-13
@@ -28,6 +28,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/android_manifest.h"
|
||||
#include "update/apk_signature.h"
|
||||
#include "update/payload_integrity.h"
|
||||
@@ -173,10 +174,15 @@ bool EndsWith(std::string_view value, std::string_view suffix) {
|
||||
struct ApkCandidate {
|
||||
std::filesystem::path path;
|
||||
AndroidManifestIdentity identity;
|
||||
bool has_x86_64_library = false;
|
||||
bool has_guest_library = false;
|
||||
bool has_assets = false;
|
||||
};
|
||||
|
||||
std::string GuestLibEntryName() {
|
||||
return std::string(compat::kGuestLibApkEntryPrefix) +
|
||||
std::string(compat::kGuestAbi) + "/libroblox.so";
|
||||
}
|
||||
|
||||
bool CollectCandidates(const std::vector<std::filesystem::path>& archives,
|
||||
const std::filesystem::path& directory,
|
||||
std::vector<std::filesystem::path>* candidates,
|
||||
@@ -250,7 +256,7 @@ bool InspectCandidates(const std::vector<std::filesystem::path>& paths,
|
||||
ApkCandidate candidate;
|
||||
candidate.path = path;
|
||||
candidate.identity = std::move(identity);
|
||||
candidate.has_x86_64_library = HasEntry(list, "lib/x86_64/libroblox.so");
|
||||
candidate.has_guest_library = HasEntry(list, GuestLibEntryName());
|
||||
candidate.has_assets = std::any_of(
|
||||
list.entries.begin(), list.entries.end(), [](const ZipEntry& entry) {
|
||||
return entry.name.size() > 7 && entry.name.substr(0, 7) == "assets/";
|
||||
@@ -314,7 +320,8 @@ std::vector<std::string> CommonTrustedCertificates(
|
||||
const ApkSignatureResult split_signature =
|
||||
base == split ? base_signature : VerifyApkSignature(split);
|
||||
if (!split_signature) {
|
||||
*error = "x86_64 APK signature: " + split_signature.error;
|
||||
*error = std::string(compat::kGuestAbi) +
|
||||
" APK signature: " + split_signature.error;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> accepted;
|
||||
@@ -356,9 +363,11 @@ bool ValidateElf(const std::filesystem::path& path, std::string* build_id,
|
||||
}
|
||||
GElf_Ehdr header{};
|
||||
if (gelf_getehdr(elf.get(), &header) == nullptr ||
|
||||
header.e_machine != EM_X86_64 || gelf_getclass(elf.get()) != ELFCLASS64) {
|
||||
header.e_machine != compat::kGuestElfMachine ||
|
||||
gelf_getclass(elf.get()) != ELFCLASS64) {
|
||||
close_elf();
|
||||
*error = "libroblox.so is not an x86-64 ELF";
|
||||
*error = "libroblox.so is not a " + std::string(compat::kGuestAbi) +
|
||||
" ELF";
|
||||
return false;
|
||||
}
|
||||
std::size_t section_count = 0;
|
||||
@@ -464,16 +473,18 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
base = &candidate;
|
||||
}
|
||||
}
|
||||
const std::string guest_split_name(compat::kGuestApkSplitName);
|
||||
for (const ApkCandidate& candidate : candidates) {
|
||||
if (candidate.has_x86_64_library &&
|
||||
(candidate.identity.split_name == "config.x86_64" ||
|
||||
if (candidate.has_guest_library &&
|
||||
(candidate.identity.split_name == guest_split_name ||
|
||||
candidate.identity.split_name.empty()) &&
|
||||
split == nullptr) {
|
||||
split = &candidate;
|
||||
}
|
||||
}
|
||||
if (base == nullptr || split == nullptr) {
|
||||
result.error = "no matching Roblox base and x86_64 APK pair was found";
|
||||
result.error = "no matching Roblox base and " +
|
||||
std::string(compat::kGuestAbi) + " APK pair was found";
|
||||
return result;
|
||||
}
|
||||
const std::set<std::string> trusted =
|
||||
@@ -488,12 +499,13 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
if (filesystem_error ||
|
||||
!CopyRegular(base->path, prepared / "sober_apk/base.apk",
|
||||
&result.error) ||
|
||||
!CopyRegular(split->path, prepared / "sober_apk/split_config.x86_64.apk",
|
||||
!CopyRegular(split->path,
|
||||
prepared / "sober_apk" / compat::kGuestSplitApkFile,
|
||||
&result.error)) {
|
||||
if (result.error.empty()) result.error = "cannot create prepared payload";
|
||||
return result;
|
||||
}
|
||||
if (!ExtractZipEntry(split->path, "lib/x86_64/libroblox.so",
|
||||
if (!ExtractZipEntry(split->path, GuestLibEntryName(),
|
||||
prepared / "libroblox.so", kMaximumApkBytes,
|
||||
&result.error)) {
|
||||
return result;
|
||||
@@ -527,7 +539,7 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
const std::string base_hash =
|
||||
HashRegularFile(prepared / "sober_apk/base.apk", &hash_error);
|
||||
const std::string split_hash = HashRegularFile(
|
||||
prepared / "sober_apk/split_config.x86_64.apk", &hash_error);
|
||||
prepared / "sober_apk" / compat::kGuestSplitApkFile, &hash_error);
|
||||
if (!hash_error.empty()) {
|
||||
result.error = hash_error;
|
||||
return result;
|
||||
@@ -537,12 +549,12 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
{"package", "com.roblox.client"},
|
||||
{"version_name", expected.version_name},
|
||||
{"version_code", expected.version_code},
|
||||
{"abi", "x86_64"},
|
||||
{"abi", std::string(compat::kGuestAbi)},
|
||||
{"elf_build_id", build_id},
|
||||
{"sha256",
|
||||
{{"libroblox", library_hash},
|
||||
{"base_apk", base_hash},
|
||||
{"x86_64_split_apk", split_hash}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey), split_hash}}},
|
||||
{"signing_certificates_sha256", accepted},
|
||||
{"assets",
|
||||
{{"file_count", asset_count_verified},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/http_download.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -199,7 +200,8 @@ std::vector<std::string> ParseApkPureExactDownloadUrls(
|
||||
}
|
||||
|
||||
ProviderVersion ApkPureProvider::CheckLatest() const {
|
||||
const HttpBytesResult metadata = FetchMetadata("x86_64");
|
||||
const HttpBytesResult metadata =
|
||||
FetchMetadata(std::string(compat::kGuestAbi));
|
||||
if (!metadata) return ProviderVersion{{}, 0, metadata.error};
|
||||
return ParseApkPureLatestMetadata(metadata.bytes);
|
||||
}
|
||||
@@ -223,7 +225,7 @@ ProviderDownloadResult ApkPureProvider::DownloadExact(
|
||||
result.error = "APKPure output directory must exist and be empty";
|
||||
return result;
|
||||
}
|
||||
HttpBytesResult metadata = FetchMetadata("x86_64");
|
||||
HttpBytesResult metadata = FetchMetadata(std::string(compat::kGuestAbi));
|
||||
if (!metadata) {
|
||||
result.error = metadata.error;
|
||||
return result;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/payload_integrity.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -260,7 +261,7 @@ std::string PayloadRuntimeFingerprint(
|
||||
evidence.append("base_apk=")
|
||||
.append(payload.metadata.base_apk_sha256)
|
||||
.append("\n");
|
||||
evidence.append("x86_64_split=")
|
||||
evidence.append(std::string(compat::kGuestAbi) + "_split=")
|
||||
.append(payload.metadata.split_apk_sha256)
|
||||
.append("\n");
|
||||
evidence.append("assets=")
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <cerrno>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
namespace {
|
||||
@@ -81,6 +84,16 @@ CompatibilityCatalogResult LoadCompatibilityCatalog(
|
||||
profile.value("allow_legacy_binary_patches", true)) {
|
||||
continue;
|
||||
}
|
||||
const auto declared_abi = profile.find("abi");
|
||||
if (declared_abi != profile.end() && !declared_abi->is_string()) {
|
||||
result.error = "supported compatibility profile is incomplete";
|
||||
return result;
|
||||
}
|
||||
const std::string_view profile_abi =
|
||||
declared_abi == profile.end()
|
||||
? std::string_view("x86_64")
|
||||
: std::string_view(declared_abi->get_ref<const std::string&>());
|
||||
if (profile_abi != compat::kGuestAbi) continue;
|
||||
if (!profile.contains("version_name") ||
|
||||
!profile["version_name"].is_string() ||
|
||||
!profile.contains("version_code") ||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/fmod_output_device_contract.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/payload_integrity.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -220,9 +221,10 @@ class ElfImage final {
|
||||
if (elf_ == nullptr || elf_kind(elf_) != ELF_K_ELF ||
|
||||
gelf_getehdr(elf_, &header) == nullptr ||
|
||||
gelf_getclass(elf_) != ELFCLASS64 || header.e_type != ET_DYN ||
|
||||
header.e_machine != EM_X86_64 || header.e_version != EV_CURRENT ||
|
||||
header.e_machine != compat::kGuestElfMachine ||
|
||||
header.e_version != EV_CURRENT ||
|
||||
header.e_ident[EI_DATA] != ELFDATA2LSB) {
|
||||
*error = "candidate must be a little-endian x86-64 ET_DYN ELF";
|
||||
*error = "candidate must be a little-endian guest-abi ET_DYN ELF";
|
||||
return false;
|
||||
}
|
||||
std::size_t program_count = 0;
|
||||
@@ -1879,7 +1881,7 @@ std::optional<Json> LoadCandidateMetadata(const std::filesystem::path& root,
|
||||
metadata->contains("sha256") ? &(*metadata)["sha256"] : nullptr;
|
||||
if (metadata->value("schema_version", 0) != 1 ||
|
||||
metadata->value("package", "") != "com.roblox.client" ||
|
||||
metadata->value("abi", "") != "x86_64" || version_name.empty() ||
|
||||
metadata->value("abi", "") != compat::kGuestAbi || version_name.empty() ||
|
||||
version_code == 0 ||
|
||||
metadata->value("elf_build_id", "") != candidate.build_id() ||
|
||||
hashes == nullptr || !hashes->is_object() ||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
namespace {
|
||||
|
||||
@@ -291,7 +293,7 @@ PayloadIntegrityResult InspectPreparedPayload(
|
||||
for (const std::filesystem::path& relative : {
|
||||
std::filesystem::path("libroblox.so"),
|
||||
std::filesystem::path("sober_apk/base.apk"),
|
||||
std::filesystem::path("sober_apk/split_config.x86_64.apk"),
|
||||
std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
|
||||
std::filesystem::path("roblox_payload.json"),
|
||||
}) {
|
||||
const auto status =
|
||||
@@ -335,7 +337,7 @@ PayloadIntegrityResult InspectPreparedPayload(
|
||||
result.metadata.library_sha256 = document["sha256"].value("libroblox", "");
|
||||
result.metadata.base_apk_sha256 = document["sha256"].value("base_apk", "");
|
||||
result.metadata.split_apk_sha256 =
|
||||
document["sha256"].value("x86_64_split_apk", "");
|
||||
document["sha256"].value(std::string(compat::kGuestSplitApkHashKey), "");
|
||||
if (!document["assets"].contains("file_count") ||
|
||||
!document["assets"]["file_count"].is_number_unsigned()) {
|
||||
result.error = "payload metadata asset count is invalid";
|
||||
@@ -368,7 +370,8 @@ PayloadIntegrityResult VerifyPreparedPayload(
|
||||
const std::array<std::pair<std::filesystem::path, std::string>, 3> files = {{
|
||||
{"libroblox.so", result.metadata.library_sha256},
|
||||
{"sober_apk/base.apk", result.metadata.base_apk_sha256},
|
||||
{"sober_apk/split_config.x86_64.apk", result.metadata.split_apk_sha256},
|
||||
{std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
|
||||
result.metadata.split_apk_sha256},
|
||||
}};
|
||||
for (const auto& [relative, expected] : files) {
|
||||
const std::string actual =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Includes changes by vii from CoderDayton/nightcap.
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
@@ -106,13 +107,19 @@ target_link_libraries(stub_glesv2 PRIVATE dl)
|
||||
add_library(stub_vulkan SHARED
|
||||
${CMAKE_SOURCE_DIR}/src/graphics/bionic_vulkan_loader_adapter.cc
|
||||
${CMAKE_SOURCE_DIR}/src/graphics/vulkan_text_overlay_compositor.cc
|
||||
${CMAKE_SOURCE_DIR}/src/graphics/vulkan_etc2_emulation.cc
|
||||
${CMAKE_SOURCE_DIR}/src/graphics/etc2_decoder.cc
|
||||
${CMAKE_SOURCE_DIR}/src/graphics/texture_override.cc
|
||||
)
|
||||
configure_stub(stub_vulkan "libvulkan.so")
|
||||
target_include_directories(stub_vulkan PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
pkg_check_modules(LIBPLACEBO REQUIRED IMPORTED_TARGET libplacebo)
|
||||
pkg_check_modules(LIBPNG REQUIRED IMPORTED_TARGET libpng)
|
||||
target_link_libraries(stub_vulkan PRIVATE
|
||||
Mocktail::SdlVulkanWsi
|
||||
PkgConfig::LIBPLACEBO
|
||||
PkgConfig::LIBPNG
|
||||
Threads::Threads
|
||||
dl
|
||||
)
|
||||
|
||||
|
||||
@@ -996,6 +996,12 @@ add_test(
|
||||
${CMAKE_SOURCE_DIR}/packaging/AppRun
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AnyLinuxDependenciesTest.RollingAssetsAreVerified
|
||||
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/anylinux_dependencies_test.sh
|
||||
${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AnyLinuxPackagingTest.RelocatedRuntimePaths
|
||||
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/anylinux_packaging_test.sh
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly ROOT="${1:?source root is required}"
|
||||
readonly TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-dependency-test.XXXXXX")"
|
||||
trap 'rm -rf -- "${TEMP_DIR}"' EXIT
|
||||
source "${ROOT}/scripts/install_anylinux_dependencies.sh"
|
||||
|
||||
Fail() {
|
||||
printf 'AnyLinux dependency test failed: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
readonly API=https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases
|
||||
readonly FFMPEG=ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
readonly OPUS=opus-mini-x86_64.pkg.tar.zst
|
||||
printf 'ffmpeg fixture\n' >"${TEMP_DIR}/ffmpeg"
|
||||
printf 'updated ffmpeg fixture\n' >"${TEMP_DIR}/updated-ffmpeg"
|
||||
printf 'opus fixture\n' >"${TEMP_DIR}/opus"
|
||||
readonly FFMPEG_HASH="$(sha256sum "${TEMP_DIR}/ffmpeg" | cut -d ' ' -f 1)"
|
||||
readonly UPDATED_FFMPEG_HASH="$(sha256sum "${TEMP_DIR}/updated-ffmpeg" | cut -d ' ' -f 1)"
|
||||
readonly OPUS_HASH="$(sha256sum "${TEMP_DIR}/opus" | cut -d ' ' -f 1)"
|
||||
printf '# Required fixture packages\n\n%s\n%s' "${FFMPEG}" "${OPUS}" \
|
||||
>"${TEMP_DIR}/manifest.txt"
|
||||
jq --null-input --arg api "${API}" --arg ffmpeg "${FFMPEG}" --arg opus "${OPUS}" \
|
||||
--arg ffmpeg_hash "${FFMPEG_HASH}" --arg opus_hash "${OPUS_HASH}" '
|
||||
{tag_name: "continuous", assets: [
|
||||
{id: 10, name: $ffmpeg, state: "uploaded", digest: ("sha256:" + $ffmpeg_hash),
|
||||
url: ($api + "/assets/10")},
|
||||
{id: 20, name: $opus, state: "uploaded", digest: ("sha256:" + $opus_hash),
|
||||
url: ($api + "/assets/20")},
|
||||
{id: 30, name: "unrequested-mini-x86_64.pkg.tar.zst", state: "uploaded",
|
||||
digest: null, url: ($api + "/assets/30")}
|
||||
]}
|
||||
' >"${TEMP_DIR}/release.json"
|
||||
jq --arg hash "${UPDATED_FFMPEG_HASH}" '
|
||||
.assets[0].id = 11 | .assets[0].url |= sub("/10$"; "/11") |
|
||||
.assets[0].digest = ("sha256:" + $hash) |
|
||||
.assets[1].id = 21 | .assets[1].url |= sub("/20$"; "/21")
|
||||
' \
|
||||
"${TEMP_DIR}/release.json" >"${TEMP_DIR}/refreshed-release.json"
|
||||
mkdir -p "${TEMP_DIR}/project/scripts" "${TEMP_DIR}/project/packaging"
|
||||
cp "${ROOT}/scripts/install_anylinux_dependencies.sh" "${TEMP_DIR}/project/scripts/"
|
||||
cp "${TEMP_DIR}/manifest.txt" "${TEMP_DIR}/project/packaging/anylinux-dependencies.txt"
|
||||
cp "${ROOT}/packaging/anylinux-dependencies-aarch64.txt" \
|
||||
"${TEMP_DIR}/project/packaging/"
|
||||
|
||||
# Record requests without any network access or changes to host packages.
|
||||
curl() {
|
||||
local output="" accept="" authorization="" url=""
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--output) output="$2"; shift 2 ;;
|
||||
--header)
|
||||
case "$2" in
|
||||
Accept:*) accept="$2" ;;
|
||||
Authorization:*) authorization="$2" ;;
|
||||
esac
|
||||
shift 2 ;;
|
||||
--retry|--proto|--proto-redir|--connect-timeout|--max-time) shift 2 ;;
|
||||
--fail|--silent|--show-error|--location) shift ;;
|
||||
https://*) url="$1"; shift ;;
|
||||
*) Fail "unexpected curl option: $1" ;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "${GH_TOKEN:-}" ]]; then
|
||||
[[ "${authorization}" == "Authorization: Bearer ${GH_TOKEN}" ]] ||
|
||||
Fail 'missing authenticated GitHub request'
|
||||
fi
|
||||
printf '%s\n' "${url}" >>"${TEMP_DIR}/calls"
|
||||
case "${url}" in
|
||||
"${API}/tags/continuous")
|
||||
[[ "${accept}" == 'Accept: application/vnd.github+json' ]]
|
||||
if [[ "${SCENARIO}" == metadata-failure ]]; then return 22; fi
|
||||
if [[ "${SCENARIO}" == replaced && -f "${TEMP_DIR}/asset-replaced" ]]; then
|
||||
cp "${TEMP_DIR}/refreshed-release.json" "${output}"
|
||||
else
|
||||
cp "${TEST_RELEASE}" "${output}"
|
||||
fi ;;
|
||||
"${API}/assets/10"|"${API}/assets/11"|"${API}/assets/20"|"${API}/assets/21")
|
||||
[[ "${accept}" == 'Accept: application/octet-stream' ]]
|
||||
if [[ "${SCENARIO}" == download-failure ]]; then return 22; fi
|
||||
if [[ "${SCENARIO}" == replaced && "${url}" == "${API}/assets/20" ]]; then
|
||||
touch "${TEMP_DIR}/asset-replaced"
|
||||
return 22
|
||||
fi
|
||||
if [[ "${url}" == "${API}/assets/20" || "${url}" == "${API}/assets/21" ]]; then
|
||||
cp "${TEMP_DIR}/opus" "${output}"
|
||||
elif [[ "${url}" == "${API}/assets/11" ]]; then
|
||||
cp "${TEMP_DIR}/updated-ffmpeg" "${output}"
|
||||
else
|
||||
cp "${TEMP_DIR}/ffmpeg" "${output}"
|
||||
fi
|
||||
if [[ "${SCENARIO}" == corrupted && "${url}" == "${API}/assets/20" ]]; then
|
||||
printf 'corrupted\n' >>"${output}"
|
||||
fi ;;
|
||||
*) Fail "unrequested or rolling asset URL: ${url}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
pacman() {
|
||||
[[ "$#" == 5 && "$1" == -U && "$2" == --noconfirm && "$3" == --needed ]]
|
||||
if [[ "${SCENARIO}" == replaced ]]; then
|
||||
cmp "${TEMP_DIR}/updated-ffmpeg" "$4"
|
||||
else
|
||||
cmp "${TEMP_DIR}/ffmpeg" "$4"
|
||||
fi
|
||||
cmp "${TEMP_DIR}/opus" "$5"
|
||||
local downloads="${4%/*}"
|
||||
(cd "${downloads}" && sha256sum --check --strict SHA256SUMS)
|
||||
cp "$4" "$5" "${downloads}/SHA256SUMS" "${downloads}/dependencies.tsv" \
|
||||
"${TEMP_DIR}/${SCENARIO}/"
|
||||
touch "${TEMP_DIR}/installed"
|
||||
}
|
||||
|
||||
export -f curl pacman Fail
|
||||
export TEMP_DIR API FFMPEG OPUS SCENARIO TEST_RELEASE
|
||||
|
||||
ExpectRejectedSnapshot() {
|
||||
local description="$1" filter="$2"
|
||||
jq "${filter}" "${TEMP_DIR}/release.json" >"${TEMP_DIR}/invalid.json"
|
||||
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
|
||||
"${TEMP_DIR}/invalid.json" x86_64) >"${TEMP_DIR}/invalid.log" 2>&1; then
|
||||
Fail "accepted ${description}"
|
||||
fi
|
||||
}
|
||||
|
||||
(AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
|
||||
"${TEMP_DIR}/release.json" x86_64) >"${TEMP_DIR}/snapshot.tsv" ||
|
||||
Fail 'rejected valid dependency snapshot'
|
||||
printf '%s\t%s\t%s\n' \
|
||||
"${FFMPEG_HASH}" "${FFMPEG}" "${API}/assets/10" \
|
||||
"${OPUS_HASH}" "${OPUS}" "${API}/assets/20" \
|
||||
>"${TEMP_DIR}/expected-snapshot.tsv"
|
||||
cmp "${TEMP_DIR}/expected-snapshot.tsv" "${TEMP_DIR}/snapshot.tsv" ||
|
||||
Fail 'incorrect dependency snapshot'
|
||||
|
||||
readonly ARM_PACKAGE=gdk-pixbuf2-mini-aarch64.pkg.tar.xz
|
||||
printf '%s\n' "${ARM_PACKAGE}" >"${TEMP_DIR}/arm-manifest.txt"
|
||||
jq --null-input --arg api "${API}" --arg name "${ARM_PACKAGE}" \
|
||||
--arg hash "${FFMPEG_HASH}" '
|
||||
{tag_name: "continuous", assets: [
|
||||
{id: 40, name: $name, state: "uploaded",
|
||||
digest: ("sha256:" + $hash), url: ($api + "/assets/40")}
|
||||
]}
|
||||
' >"${TEMP_DIR}/arm-release.json"
|
||||
AnyLinuxDependencySnapshot "${TEMP_DIR}/arm-manifest.txt" \
|
||||
"${TEMP_DIR}/arm-release.json" aarch64 >"${TEMP_DIR}/arm-snapshot.tsv" ||
|
||||
Fail 'rejected valid aarch64 dependency snapshot'
|
||||
printf '%s\t%s\t%s\n' "${FFMPEG_HASH}" "${ARM_PACKAGE}" \
|
||||
"${API}/assets/40" >"${TEMP_DIR}/expected-arm-snapshot.tsv"
|
||||
cmp "${TEMP_DIR}/expected-arm-snapshot.tsv" "${TEMP_DIR}/arm-snapshot.tsv" ||
|
||||
Fail 'incorrect aarch64 dependency snapshot'
|
||||
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/arm-manifest.txt" \
|
||||
"${TEMP_DIR}/arm-release.json" x86_64) >/dev/null 2>&1; then
|
||||
Fail 'accepted an aarch64 dependency on an x86_64 host'
|
||||
fi
|
||||
bash -Eeuo pipefail -c '
|
||||
source "$1/project/scripts/install_anylinux_dependencies.sh"
|
||||
AnyLinuxDependencyRequireContainer() { :; }
|
||||
uname() { printf "aarch64\n"; }
|
||||
AnyLinuxDownloadDependencies() {
|
||||
printf "%s\n" "$1" >"${TEMP_DIR}/arm-selected-manifest"
|
||||
exit 0
|
||||
}
|
||||
AnyLinuxDependenciesMain
|
||||
' _ "${TEMP_DIR}" || Fail 'aarch64 manifest selection failed'
|
||||
grep -Fxq "${TEMP_DIR}/project/packaging/anylinux-dependencies-aarch64.txt" \
|
||||
"${TEMP_DIR}/arm-selected-manifest" ||
|
||||
Fail 'aarch64 build did not select its dependency manifest'
|
||||
|
||||
ExpectRejectedSnapshot 'missing asset' '.assets |= .[1:]'
|
||||
ExpectRejectedSnapshot 'duplicate asset' '.assets += [.assets[0]]'
|
||||
ExpectRejectedSnapshot 'absent SHA256' '.assets[0].digest = null'
|
||||
ExpectRejectedSnapshot 'malformed SHA256' '.assets[0].digest = "sha256:bad"'
|
||||
ExpectRejectedSnapshot 'foreign asset URL' '.assets[0].url = "https://example.org/archive"'
|
||||
ExpectRejectedSnapshot 'fractional asset ID' '.assets[0].id = 10.5'
|
||||
ExpectRejectedSnapshot 'unuploaded asset' '.assets[0].state = "starter"'
|
||||
ExpectRejectedSnapshot 'different release' '.tag_name = "other"'
|
||||
|
||||
for invalid_manifest in '../ffmpeg-mini-x86_64.pkg.tar.zst' \
|
||||
"${FFMPEG} extra" "${FFMPEG}"$'\n'"${FFMPEG}" ''; do
|
||||
printf '%s\n' "${invalid_manifest}" >"${TEMP_DIR}/invalid-manifest.txt"
|
||||
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/invalid-manifest.txt" \
|
||||
"${TEMP_DIR}/release.json" x86_64) >"${TEMP_DIR}/invalid.log" 2>&1; then
|
||||
Fail 'accepted unsafe, duplicate, or empty package allowlist'
|
||||
fi
|
||||
done
|
||||
|
||||
for SCENARIO in success replaced corrupted download-failure metadata-failure invalid-metadata; do
|
||||
TEST_RELEASE="${TEMP_DIR}/release.json"
|
||||
if [[ "${SCENARIO}" == invalid-metadata ]]; then
|
||||
# A valid first row must not allow downloads when a later row is invalid.
|
||||
jq '.assets[1].digest = null' "${TEMP_DIR}/release.json" >"${TEMP_DIR}/invalid.json"
|
||||
TEST_RELEASE="${TEMP_DIR}/invalid.json"
|
||||
fi
|
||||
mkdir "${TEMP_DIR}/${SCENARIO}"
|
||||
: >"${TEMP_DIR}/calls"
|
||||
if GH_TOKEN=fixture-token bash -Eeuo pipefail -c '
|
||||
source "$1/project/scripts/install_anylinux_dependencies.sh"
|
||||
AnyLinuxDependencyRequireContainer() { :; }
|
||||
uname() { printf "x86_64\n"; }
|
||||
AnyLinuxDependenciesMain
|
||||
' _ "${TEMP_DIR}" \
|
||||
>"${TEMP_DIR}/${SCENARIO}.log" 2>&1; then
|
||||
[[ "${SCENARIO}" == success || "${SCENARIO}" == replaced ]] ||
|
||||
Fail "installed packages after ${SCENARIO}"
|
||||
[[ -f "${TEMP_DIR}/installed" ]]
|
||||
(cd "${TEMP_DIR}/${SCENARIO}" && sha256sum --check --strict SHA256SUMS)
|
||||
rm -- "${TEMP_DIR}/installed"
|
||||
else
|
||||
if [[ "${SCENARIO}" == success || "${SCENARIO}" == replaced ]]; then
|
||||
cat -- "${TEMP_DIR}/${SCENARIO}.log" >&2
|
||||
Fail "valid download failed: ${SCENARIO}"
|
||||
fi
|
||||
[[ ! -e "${TEMP_DIR}/installed" ]] || Fail 'installed unverified archives'
|
||||
fi
|
||||
case "${SCENARIO}" in
|
||||
success)
|
||||
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 3 ]]
|
||||
grep -Fq "${API}/assets/10" "${TEMP_DIR}/calls" ;;
|
||||
replaced)
|
||||
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 6 ]]
|
||||
grep -Fq "${API}/assets/11" "${TEMP_DIR}/calls"
|
||||
grep -Fq "${API}/assets/11" "${TEMP_DIR}/replaced/dependencies.tsv"
|
||||
grep -Fq "${API}/assets/21" "${TEMP_DIR}/replaced/dependencies.tsv" ;;
|
||||
corrupted)
|
||||
grep -Fq 'dependency checksum verification failed' "${TEMP_DIR}/${SCENARIO}.log" ;;
|
||||
download-failure)
|
||||
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 6 ]]
|
||||
grep -Fq 'failed after 3 snapshots' "${TEMP_DIR}/${SCENARIO}.log" ;;
|
||||
metadata-failure|invalid-metadata)
|
||||
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 1 ]] ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (id() { printf '1000\n'; }; AnyLinuxDependencyRequireContainer) \
|
||||
>"${TEMP_DIR}/guard.log" 2>&1; then
|
||||
Fail 'container/root installation guard was bypassed'
|
||||
fi
|
||||
|
||||
printf 'AnyLinux dependency tests passed\n'
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "compat/bionic_abi_exports.h"
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
@@ -37,8 +39,10 @@ TEST(BionicAbiExportsTest, ReportsStableAndroidSystemProperties) {
|
||||
EXPECT_STREQ("35", value.data());
|
||||
|
||||
value.fill('\0');
|
||||
EXPECT_EQ(6, __system_property_get("ro.product.cpu.abi", value.data()));
|
||||
EXPECT_STREQ("x86_64", value.data());
|
||||
EXPECT_EQ(static_cast<int>(mocktail::compat::kGuestAbi.size()),
|
||||
__system_property_get("ro.product.cpu.abi", value.data()));
|
||||
EXPECT_STREQ(std::string(mocktail::compat::kGuestAbi).c_str(),
|
||||
value.data());
|
||||
|
||||
value.fill('x');
|
||||
EXPECT_EQ(0, __system_property_get("mocktail.unknown", value.data()));
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "compat/bionic_abi_exports.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
||||
@@ -189,9 +191,10 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) {
|
||||
0);
|
||||
ASSERT_EQ(pthread_attr_destroy(&host_defaults), 0);
|
||||
|
||||
pthread_attr_t guest_attributes;
|
||||
ASSERT_EQ(pthread_attr_init(&guest_attributes), 0);
|
||||
ASSERT_EQ(pthread_attr_setstacksize(&guest_attributes, kRequestedStackSize),
|
||||
MocktailBionicPthreadAttr guest_attributes;
|
||||
ASSERT_EQ(mocktail_pthread_attr_init(&guest_attributes), 0);
|
||||
ASSERT_EQ(mocktail_pthread_attr_setstacksize(&guest_attributes,
|
||||
kRequestedStackSize),
|
||||
0);
|
||||
|
||||
g_intercepted_create_calls.store(0, std::memory_order_release);
|
||||
@@ -206,7 +209,7 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) {
|
||||
&thread, &guest_attributes, RecordThreadStackSize, nullptr);
|
||||
g_intercept_pthread_create.store(false, std::memory_order_release);
|
||||
|
||||
ASSERT_EQ(pthread_attr_destroy(&guest_attributes), 0);
|
||||
ASSERT_EQ(mocktail_pthread_attr_destroy(&guest_attributes), 0);
|
||||
ASSERT_EQ(result, 0);
|
||||
ASSERT_EQ(pthread_join(thread, nullptr), 0);
|
||||
EXPECT_EQ(g_intercepted_create_calls.load(std::memory_order_acquire), 3);
|
||||
@@ -223,15 +226,16 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) {
|
||||
constexpr size_t kRequestedStackSize = 2 * 1024 * 1024;
|
||||
g_observed_stack_size.store(0, std::memory_order_release);
|
||||
|
||||
pthread_attr_t attributes;
|
||||
ASSERT_EQ(pthread_attr_init(&attributes), 0);
|
||||
ASSERT_EQ(pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0);
|
||||
MocktailBionicPthreadAttr attributes;
|
||||
ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0);
|
||||
ASSERT_EQ(
|
||||
mocktail_pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0);
|
||||
|
||||
pthread_t thread{};
|
||||
ASSERT_EQ(mocktail_bionic_pthread_create(&thread, &attributes,
|
||||
RecordThreadStackSize, nullptr),
|
||||
0);
|
||||
EXPECT_EQ(pthread_attr_destroy(&attributes), 0);
|
||||
EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0);
|
||||
ASSERT_EQ(pthread_join(thread, nullptr), 0);
|
||||
ExpectRequestedStackSize(
|
||||
g_observed_stack_size.load(std::memory_order_acquire),
|
||||
@@ -240,13 +244,13 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) {
|
||||
|
||||
TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) {
|
||||
for (const int policy : {SCHED_FIFO, SCHED_RR}) {
|
||||
pthread_attr_t attributes;
|
||||
ASSERT_EQ(pthread_attr_init(&attributes), 0);
|
||||
ASSERT_EQ(pthread_attr_setinheritsched(&attributes, PTHREAD_EXPLICIT_SCHED), 0);
|
||||
ASSERT_EQ(pthread_attr_setschedpolicy(&attributes, policy), 0);
|
||||
MocktailBionicPthreadAttr attributes;
|
||||
ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0);
|
||||
attributes.sched_policy = policy;
|
||||
sched_param parameters{};
|
||||
parameters.sched_priority = 99;
|
||||
ASSERT_EQ(pthread_attr_setschedparam(&attributes, ¶meters), 0);
|
||||
ASSERT_EQ(
|
||||
mocktail_pthread_attr_setschedparam(&attributes, ¶meters), 0);
|
||||
g_intercepted_create_calls.store(0);
|
||||
g_intercept_pthread_create.store(true);
|
||||
pthread_t thread{};
|
||||
@@ -255,7 +259,7 @@ TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) {
|
||||
g_intercept_pthread_create.store(false);
|
||||
EXPECT_EQ(result, EPERM);
|
||||
EXPECT_EQ(g_intercepted_create_calls.load(), 0);
|
||||
EXPECT_EQ(pthread_attr_destroy(&attributes), 0);
|
||||
EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
@@ -274,8 +276,10 @@ TEST(CommandLineTest, UsageContainsEverySupportedOption) {
|
||||
EXPECT_EQ(usage.find("--login"), std::string::npos);
|
||||
EXPECT_EQ(usage.find("--import-cookie"), std::string::npos);
|
||||
EXPECT_NE(usage.find("native sign-in flow"), std::string::npos);
|
||||
EXPECT_NE(usage.find("verified managed x86_64 Roblox payload"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(
|
||||
usage.find("verified managed " +
|
||||
std::string(mocktail::compat::kGuestAbi) + " Roblox payload"),
|
||||
std::string::npos);
|
||||
EXPECT_EQ(usage.find("Normal startup uses rbx_bin/libroblox.so"),
|
||||
std::string::npos);
|
||||
EXPECT_EQ(usage.find("--launch-request-json"), std::string::npos);
|
||||
|
||||
@@ -161,6 +161,12 @@ grep -Fq -- '--default-branch=stable' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not produce the stable Flatpak branch'
|
||||
grep -Fq 'Mocktail-x86_64.flatpak' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not build the installable Flatpak bundle'
|
||||
grep -Fq 'Mocktail-aarch64.flatpak' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not build the aarch64 Flatpak bundle'
|
||||
grep -Fq 'ubuntu-24.04-arm' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not build Flatpak on an ARM64 runner'
|
||||
grep -Fq 'flatpak build-import-bundle' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not merge both architectures into one repository'
|
||||
grep -Fq 'actions/upload-artifact@' "${GITHUB_CI}" ||
|
||||
Fail 'GitHub Actions does not publish the Flatpak artifact'
|
||||
grep -Fq 'actions/upload-pages-artifact@' "${GITHUB_CI}" ||
|
||||
@@ -209,18 +215,35 @@ set -e
|
||||
grep -Fq 'flatpak-builder is unavailable' "${TEMP_DIR}/stderr" ||
|
||||
Fail 'helper did not explain how to install flatpak-builder'
|
||||
|
||||
cat >"${TEMP_DIR}/bin/uname" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf 'aarch64\n'
|
||||
EOF
|
||||
cat >"${TEMP_DIR}/bin/flatpak-builder" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$@" >"${MOCKTAIL_TEST_BUILDER_ARGS}"
|
||||
EOF
|
||||
chmod 0755 "${TEMP_DIR}/bin/flatpak-builder"
|
||||
PATH="${TEMP_DIR}/bin:/usr/bin:/bin" \
|
||||
MOCKTAIL_TEST_BUILDER_ARGS="${TEMP_DIR}/builder-args" \
|
||||
"${BUILD_HELPER}" --build-dir "${ROOT}/build-flatpak-test"
|
||||
grep -Fxq -- '--arch=aarch64' "${TEMP_DIR}/builder-args" ||
|
||||
Fail 'local Flatpak helper did not select aarch64'
|
||||
|
||||
mkdir -p -- "${TEMP_DIR}/repo/objects"
|
||||
mkdir -p -- \
|
||||
"${TEMP_DIR}/native-repositories/apt" \
|
||||
"${TEMP_DIR}/native-repositories/rpm" \
|
||||
"${TEMP_DIR}/native-repositories/downloads"
|
||||
printf 'bundle\n' >"${TEMP_DIR}/Mocktail-x86_64.flatpak"
|
||||
printf 'bundle\n' >"${TEMP_DIR}/Mocktail-aarch64.flatpak"
|
||||
printf 'public-key\n' >"${TEMP_DIR}/mocktail-flatpak.gpg"
|
||||
printf 'public-key\n' \
|
||||
>"${TEMP_DIR}/native-repositories/mocktail-packages.gpg"
|
||||
"${PAGES_HELPER}" \
|
||||
"${TEMP_DIR}/repo" \
|
||||
"${TEMP_DIR}/Mocktail-x86_64.flatpak" \
|
||||
"${TEMP_DIR}/Mocktail-aarch64.flatpak" \
|
||||
"${TEMP_DIR}/mocktail-flatpak.gpg" \
|
||||
"${TEMP_DIR}/public" \
|
||||
"${TEMP_DIR}/native-repositories"
|
||||
@@ -230,8 +253,13 @@ grep -Fq 'Url=https://mocktail.bigrat.space/repo/' \
|
||||
grep -Fq 'Name=space.bigrat.mocktail' \
|
||||
"${TEMP_DIR}/public/mocktail.flatpakref" ||
|
||||
Fail 'published Flatpak ref has the wrong application ID'
|
||||
[[ -f "${TEMP_DIR}/public/Mocktail-x86_64.flatpak" &&
|
||||
-f "${TEMP_DIR}/public/Mocktail-aarch64.flatpak" ]] ||
|
||||
Fail 'Pages output does not include both Flatpak bundles'
|
||||
grep -Fq 'flatpak install --user' "${TEMP_DIR}/public/index.html" ||
|
||||
Fail 'Pages landing page has no direct installation command'
|
||||
grep -Fq 'Mocktail-aarch64.flatpak' "${TEMP_DIR}/public/index.html" ||
|
||||
Fail 'Pages landing page has no aarch64 Flatpak bundle link'
|
||||
grep -Fq 'roblox://experiences/start' "${TEMP_DIR}/public/join.html" ||
|
||||
Fail 'Pages output has no Discord join bridge'
|
||||
[[ -d "${TEMP_DIR}/public/apt" &&
|
||||
|
||||
@@ -102,10 +102,16 @@ for expected in \
|
||||
'MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1' \
|
||||
'--appimage-extract-and-run mocktail_updater status' \
|
||||
'Mocktail-x86_64.AppImage' \
|
||||
'Mocktail-aarch64.AppImage' \
|
||||
'mocktail-nightly-aarch64.AppImage' \
|
||||
'ubuntu-24.04-arm' \
|
||||
'gh release upload continuous'; do
|
||||
grep -Fq -- "${expected}" "${WORKFLOW}" ||
|
||||
Fail "native package workflow is missing: ${expected}"
|
||||
done
|
||||
grep -Fq 'needs: [appimage, appimage-aarch64, deb, rpm, arch]' \
|
||||
"${WORKFLOW}" ||
|
||||
Fail 'continuous release does not wait for the aarch64 AppImage'
|
||||
|
||||
grep -Fq 'paths-ignore:' "${WORKFLOW}" ||
|
||||
Fail 'native package workflow has no path filters'
|
||||
|
||||
+37
-19
@@ -13,6 +13,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "compat/host_abi_profile.h"
|
||||
#include "update/apkpure_provider.h"
|
||||
#include "update/compatibility_catalog.h"
|
||||
@@ -518,6 +519,18 @@ TEST(ShippedMetadataTest, ReferenceSidecarDescribesASupportedProfile) {
|
||||
const HostAbiSidecarIdentity identity = ReadHostAbiSidecarIdentity(
|
||||
root / "config/roblox_host_abi_reference.json");
|
||||
ASSERT_TRUE(identity) << identity.error;
|
||||
std::string sidecar_abi = "x86_64";
|
||||
for (const auto& entry :
|
||||
nlohmann::json::parse(
|
||||
ReadFile(root / "config/roblox_compatibility.json"))
|
||||
.at("profiles")) {
|
||||
if (entry.value("elf_build_id", "") == identity.elf_build_id) {
|
||||
sidecar_abi = entry.value("abi", "x86_64");
|
||||
}
|
||||
}
|
||||
if (sidecar_abi != compat::kGuestAbi) {
|
||||
GTEST_SKIP() << "shipped reference sidecar targets " << sidecar_abi;
|
||||
}
|
||||
|
||||
const auto supported = std::find_if(
|
||||
catalog.profiles.begin(), catalog.profiles.end(),
|
||||
@@ -558,6 +571,11 @@ TEST(ShippedMetadataTest, DefaultPayloadHasABuiltinProfileAndMatchingReference)
|
||||
|
||||
const auto payload = nlohmann::json::parse(
|
||||
ReadFile(root / "config/roblox_payload.json"));
|
||||
if (payload.at("abi").get_ref<const std::string&>() != compat::kGuestAbi) {
|
||||
EXPECT_NE(compat::FindHostAbiProfile(preferred->elf_build_id), nullptr);
|
||||
GTEST_SKIP() << "shipped default payload targets "
|
||||
<< payload.at("abi").get<std::string>();
|
||||
}
|
||||
EXPECT_EQ(payload.at("version_name"), preferred->version_name);
|
||||
EXPECT_EQ(payload.at("version_code"), preferred->version_code);
|
||||
EXPECT_EQ(payload.at("elf_build_id"), preferred->elf_build_id);
|
||||
@@ -643,7 +661,7 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) {
|
||||
const std::filesystem::path restaged = temporary.root() / "restaged";
|
||||
Write(prepared / "libroblox.so", "native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
|
||||
Write(prepared / "assets/content/fixture", "asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -659,8 +677,8 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) {
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
{"source", "apk-pure-native"},
|
||||
{"imported_at", "2026-09-04T03:49:16Z"},
|
||||
@@ -704,7 +722,7 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) {
|
||||
const std::filesystem::path prepared = temporary.root() / "prepared";
|
||||
Write(prepared / "libroblox.so", "native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
|
||||
Write(prepared / "assets/content/fixture", "asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -722,8 +740,8 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) {
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
};
|
||||
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
|
||||
@@ -767,7 +785,7 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) {
|
||||
const std::filesystem::path prepared = temporary.root() / "prepared";
|
||||
Write(prepared / "libroblox.so", "native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
|
||||
Write(prepared / "assets/content/fixture", "asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -785,8 +803,8 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) {
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
};
|
||||
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
|
||||
@@ -837,7 +855,7 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) {
|
||||
const std::filesystem::path prepared = temporary.root() / "prepared";
|
||||
Write(prepared / "libroblox.so", "native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
|
||||
Write(prepared / "assets/content/fixture", "asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -853,8 +871,8 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) {
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
};
|
||||
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
|
||||
@@ -897,7 +915,7 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) {
|
||||
const std::filesystem::path prepared = temporary.root() / "prepared";
|
||||
Write(prepared / "libroblox.so", "native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk");
|
||||
Write(prepared / "assets/content/fixture", "asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -913,8 +931,8 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) {
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
};
|
||||
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
|
||||
@@ -958,7 +976,7 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) {
|
||||
const std::filesystem::path prepared = temporary.root() / "prepared";
|
||||
Write(prepared / "libroblox.so", "new-native-library");
|
||||
Write(prepared / "sober_apk/base.apk", "new-base-apk");
|
||||
Write(prepared / "sober_apk/split_config.x86_64.apk", "new-split-apk");
|
||||
Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "new-split-apk");
|
||||
Write(prepared / "assets/content/fixture", "new-asset");
|
||||
std::string error;
|
||||
std::size_t asset_count = 0;
|
||||
@@ -972,13 +990,13 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) {
|
||||
{"package", "com.roblox.client"},
|
||||
{"version_name", "2.732.1043"},
|
||||
{"version_code", 2814},
|
||||
{"abi", "x86_64"},
|
||||
{"abi", std::string(compat::kGuestAbi)},
|
||||
{"elf_build_id", kBuildId},
|
||||
{"sha256",
|
||||
{{"libroblox", HashRegularFile(prepared / "libroblox.so")},
|
||||
{"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")},
|
||||
{"x86_64_split_apk",
|
||||
HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey),
|
||||
HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}},
|
||||
{"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}},
|
||||
};
|
||||
Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n");
|
||||
|
||||
@@ -145,6 +145,28 @@ TEST(SdlEventConverterTest, ConvertsFocusWithoutOwningTheSdlQueue) {
|
||||
EXPECT_TRUE(focus->focused);
|
||||
}
|
||||
|
||||
TEST(SdlEventConverterTest, PreservesHostWheelDirectionAndPrecision) {
|
||||
SDL_Event source{};
|
||||
source.type = SDL_EVENT_MOUSE_WHEEL;
|
||||
source.wheel.mouse_x = 120.0f;
|
||||
source.wheel.mouse_y = 240.0f;
|
||||
platform::PlatformEvent event;
|
||||
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
|
||||
|
||||
for (const auto direction : {SDL_MOUSEWHEEL_NORMAL, SDL_MOUSEWHEEL_FLIPPED}) {
|
||||
source.wheel.direction = direction;
|
||||
source.wheel.x = 0.25f;
|
||||
source.wheel.y = -1.5f;
|
||||
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event));
|
||||
const auto* wheel = std::get_if<platform::MouseWheelEvent>(&event.payload);
|
||||
ASSERT_NE(wheel, nullptr);
|
||||
EXPECT_FLOAT_EQ(wheel->delta_x, 0.25f);
|
||||
EXPECT_FLOAT_EQ(wheel->delta_y, -1.5f);
|
||||
EXPECT_FLOAT_EQ(wheel->mouse_x, 120.0f);
|
||||
EXPECT_FLOAT_EQ(wheel->mouse_y, 240.0f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SdlEventConverterTest, RejectsInvalidArgumentsAndUnsupportedEvents) {
|
||||
SDL_Event source{};
|
||||
source.type = SDL_EVENT_USER;
|
||||
|
||||
@@ -322,6 +322,77 @@ TEST(RobloxLaunchUriTest, ClearsOutputAndRedactsTicketOnFailure) {
|
||||
EXPECT_FALSE(ParseRobloxLaunchUri(kModernPublicLaunchUri, nullptr).ok());
|
||||
}
|
||||
|
||||
TEST(RobloxWebViewLaunchGameTest, ParsesPrivateServerPlayButton) {
|
||||
constexpr char kCommand[] =
|
||||
R"({"moduleID":"Game","functionName":"launchGame",)"
|
||||
R"("callbackID":"b8cf5fbd-f6a8-cfba-2cae-84bce46f46b2",)"
|
||||
R"("params":{"request":{"requestType":"RequestPrivateGame",)"
|
||||
R"("browserTrackerId":"1790271319250009",)"
|
||||
R"("accessCode":"664b524f-19cc-4e28-a947-e8f5286f4179",)"
|
||||
R"("placeId":"920587237","joinAttemptId":"attempt-1",)"
|
||||
R"("joinAttemptOrigin":"privateServerListJoin",)"
|
||||
R"("isPlayTogetherGame":false}}})";
|
||||
RobloxExperienceLaunchRequest request;
|
||||
|
||||
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
|
||||
|
||||
ASSERT_TRUE(status.ok()) << status.message();
|
||||
EXPECT_EQ(request.place_id, 920587237);
|
||||
EXPECT_EQ(request.access_code, "664b524f-19cc-4e28-a947-e8f5286f4179");
|
||||
EXPECT_EQ(request.join_attempt_id, "attempt-1");
|
||||
EXPECT_EQ(request.join_attempt_origin, "privateServerListJoin");
|
||||
EXPECT_EQ(request.canonical_json.find("browserTrackerId"), std::string::npos);
|
||||
ExpectCanonicalRequestMatches(request);
|
||||
}
|
||||
|
||||
TEST(RobloxWebViewLaunchGameTest, ParsesServerJobWithNumericPlaceId) {
|
||||
constexpr char kCommand[] =
|
||||
R"({"moduleID":"Game","functionName":"launchGame",)"
|
||||
R"("params":{"request":{"requestType":"RequestGameJob",)"
|
||||
R"("placeId":1962086868,"gameId":"97ea-49e4"}}})";
|
||||
RobloxExperienceLaunchRequest request;
|
||||
|
||||
const Status status = ParseRobloxWebViewLaunchGame(kCommand, &request);
|
||||
|
||||
ASSERT_TRUE(status.ok()) << status.message();
|
||||
EXPECT_EQ(request.place_id, 1962086868);
|
||||
EXPECT_EQ(request.game_instance_id, "97ea-49e4");
|
||||
ExpectCanonicalRequestMatches(request);
|
||||
}
|
||||
|
||||
TEST(RobloxWebViewLaunchGameTest, ReportsOtherCommandsAsUnsupported) {
|
||||
RobloxExperienceLaunchRequest request;
|
||||
request.place_id = 123;
|
||||
request.canonical_json = "stale";
|
||||
|
||||
const Status status = ParseRobloxWebViewLaunchGame(
|
||||
R"({"moduleID":"Game","functionName":"getSystemInfo","params":{}})",
|
||||
&request);
|
||||
|
||||
EXPECT_EQ(status.code(), StatusCode::kUnsupported);
|
||||
EXPECT_EQ(request.place_id, 0);
|
||||
EXPECT_TRUE(request.canonical_json.empty());
|
||||
EXPECT_EQ(ParseRobloxWebViewLaunchGame("not json", &request).code(),
|
||||
StatusCode::kUnsupported);
|
||||
}
|
||||
|
||||
TEST(RobloxWebViewLaunchGameTest, RejectsMalformedLaunchGame) {
|
||||
const char* const kCommands[] = {
|
||||
R"({"functionName":"launchGame"})",
|
||||
R"({"functionName":"launchGame","params":{"request":"x"}})",
|
||||
R"({"functionName":"launchGame","params":{"request":{)"
|
||||
R"("requestType":"RequestPrivateGame","placeId":"920587237"}}})",
|
||||
R"({"functionName":"launchGame","params":{"request":{)"
|
||||
R"("requestType":"RequestGame","placeId":"-1"}}})",
|
||||
};
|
||||
for (const char* command : kCommands) {
|
||||
RobloxExperienceLaunchRequest request;
|
||||
const Status status = ParseRobloxWebViewLaunchGame(command, &request);
|
||||
EXPECT_EQ(status.code(), StatusCode::kInvalidArgument) << command;
|
||||
EXPECT_EQ(request.place_id, 0) << command;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -93,8 +95,8 @@ void WriteManagedPayloadFiles(const std::filesystem::path& payload) {
|
||||
ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "sober_apk"));
|
||||
std::ofstream(payload / "libroblox.so") << "ELF fixture";
|
||||
std::ofstream(payload / "sober_apk/base.apk") << "base APK fixture";
|
||||
std::ofstream(payload / "sober_apk/split_config.x86_64.apk")
|
||||
<< "x86_64 split APK fixture";
|
||||
std::ofstream(payload / "sober_apk" / compat::kGuestSplitApkFile)
|
||||
<< "split APK fixture";
|
||||
}
|
||||
|
||||
TEST(RuntimePathsTest, DerivesXdgDefaults) {
|
||||
@@ -174,8 +176,8 @@ TEST(RuntimePathsTest, ResolvesValidatedActivePayload) {
|
||||
EXPECT_EQ(active.root, std::filesystem::canonical(payload));
|
||||
EXPECT_EQ(active.roblox_library, active.root / "libroblox.so");
|
||||
EXPECT_EQ(active.base_apk, active.root / "sober_apk/base.apk");
|
||||
EXPECT_EQ(active.x86_64_split_apk,
|
||||
active.root / "sober_apk/split_config.x86_64.apk");
|
||||
EXPECT_EQ(active.arch_split_apk,
|
||||
active.root / "sober_apk" / compat::kGuestSplitApkFile);
|
||||
EXPECT_EQ(active.assets_content, active.root / "assets/content");
|
||||
EXPECT_EQ(paths.DefaultAssetPath(), active.assets_content);
|
||||
}
|
||||
@@ -189,7 +191,7 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) {
|
||||
const std::filesystem::path payload = data / "payloads" / payload_id;
|
||||
WriteManagedPayloadFiles(payload);
|
||||
ASSERT_TRUE(std::filesystem::remove(
|
||||
payload / "sober_apk/split_config.x86_64.apk"));
|
||||
payload / "sober_apk" / compat::kGuestSplitApkFile));
|
||||
std::ofstream(data / "current.json")
|
||||
<< "{\"schema_version\":1,\"payload_id\":\"" << payload_id
|
||||
<< "\",\"payload_path\":\"payloads/" << payload_id
|
||||
@@ -205,7 +207,8 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) {
|
||||
.ResolveActivePayload();
|
||||
|
||||
EXPECT_FALSE(active);
|
||||
EXPECT_NE(active.error.find("split_config.x86_64.apk"), std::string::npos);
|
||||
EXPECT_NE(active.error.find(std::string(compat::kGuestSplitApkFile)),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) {
|
||||
@@ -496,8 +499,8 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) {
|
||||
active.root = payload;
|
||||
active.roblox_library = payload / "libroblox.so";
|
||||
active.base_apk = payload / "sober_apk/base.apk";
|
||||
active.x86_64_split_apk =
|
||||
payload / "sober_apk/split_config.x86_64.apk";
|
||||
active.arch_split_apk =
|
||||
payload / "sober_apk" / compat::kGuestSplitApkFile;
|
||||
active.assets_content = payload / "assets/content";
|
||||
|
||||
std::string error;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "runtime/graphics_launch_policy.h"
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
@@ -40,16 +42,22 @@ void WriteManifest(const std::filesystem::path& path) {
|
||||
TEST(VulkanIcdSelectionTest, PrefersTheNativeArchitectureManifest) {
|
||||
TemporaryDirectory temporary;
|
||||
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
|
||||
WriteManifest(temporary.root() / "nvidia_icd.x86_64.json");
|
||||
const std::string native_name =
|
||||
"nvidia_icd." + std::string(compat::kGuestCpuName) + ".json";
|
||||
WriteManifest(temporary.root() / native_name);
|
||||
|
||||
EXPECT_EQ(SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd"),
|
||||
(temporary.root() / "nvidia_icd.x86_64.json").string());
|
||||
(temporary.root() / native_name).string());
|
||||
}
|
||||
|
||||
TEST(VulkanIcdSelectionTest, NeverSelectsAForeignArchitectureManifest) {
|
||||
TemporaryDirectory temporary;
|
||||
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
|
||||
#if defined(__aarch64__)
|
||||
WriteManifest(temporary.root() / "radeon_icd.x86_64.json");
|
||||
#else
|
||||
WriteManifest(temporary.root() / "radeon_icd.aarch64.json");
|
||||
#endif
|
||||
|
||||
EXPECT_TRUE(
|
||||
SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd").empty());
|
||||
@@ -70,7 +78,8 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) {
|
||||
const std::filesystem::path first = temporary.root() / "first";
|
||||
const std::filesystem::path second = temporary.root() / "second";
|
||||
WriteManifest(first / "nvidia_icd.json");
|
||||
WriteManifest(second / "nvidia_icd.x86_64.json");
|
||||
WriteManifest(second / ("nvidia_icd." +
|
||||
std::string(compat::kGuestCpuName) + ".json"));
|
||||
|
||||
EXPECT_EQ(SelectVulkanIcdManifest({first, second}, "nvidia_icd"),
|
||||
(first / "nvidia_icd.json").string());
|
||||
@@ -78,13 +87,15 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) {
|
||||
|
||||
TEST(VulkanIcdSelectionTest, IsStableWhateverTheDirectoryOrderIs) {
|
||||
TemporaryDirectory temporary;
|
||||
WriteManifest(temporary.root() / "nvidia_icd.x86_64.json");
|
||||
const std::string native_name =
|
||||
"nvidia_icd." + std::string(compat::kGuestCpuName) + ".json";
|
||||
WriteManifest(temporary.root() / native_name);
|
||||
WriteManifest(temporary.root() / "nvidia_icd.json");
|
||||
WriteManifest(temporary.root() / "nvidia_icd.i686.json");
|
||||
|
||||
const std::string selected =
|
||||
SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd");
|
||||
EXPECT_EQ(selected, (temporary.root() / "nvidia_icd.x86_64.json").string());
|
||||
EXPECT_EQ(selected, (temporary.root() / native_name).string());
|
||||
EXPECT_EQ(selected, SelectVulkanIcdManifest({temporary.root()},
|
||||
"nvidia_icd"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user