Compare commits

...
Author SHA1 Message Date
vii 0f6847d6d7 fix(webview): launch web Play buttons without waiting for navigation
Roblox defers a launchGame sent from the WebView until the Lua app next
navigates, so joining a private server stalled until a sidebar tab was
clicked. Parse launchGame in the runtime, queue it through the
experience launch pipeline, and close the WebView. Other commands still
reach Roblox.

Fixes #169
2026-09-24 23:01:44 -05:00
珍惜andHttpAnimations 30d50f15d1 Fix catalog unable to tell arm64 from x86_64 profiles for the same version. (#167)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-24 20:12:18 +03:00
komaruworld ab940acfb5 add arm64 ci with build
fog, update faq
2026-09-24 19:03:22 +03:00
farwhile 719b1fdd22 fix/respect host natural scrolling direction (#163) 2026-09-23 18:31:41 +03:00
珍惜andHttpAnimations 1fa2e116b3 Fix crashing on x86 hosts (#162)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-23 12:04:53 +03:00
珍惜andHttpAnimations 40237fae72 arm 64 (#152)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-21 20:09:37 +03:00
komaruworld 854969457f Fix textures and device info from Nightcap
Ported from CoderDayton/nightcap
2026-09-18 16:55:29 +03:00
komaruworld b54c896498 ci hate me? 2026-09-15 19:52:24 +03:00
komaruworld 1cd7b7db8f Fuck ci 2026-09-15 19:42:56 +03:00
komaruworld cdbf9ac1eb revert commit fdce534 2026-09-15 18:26:13 +03:00
komaruworld f383fbbff4 update faq.md 2026-09-15 12:08:18 +03:00
77 changed files with 4483 additions and 394 deletions

No files matched your search

+51
View File
@@ -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
View File
@@ -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:
+122 -7
View File
@@ -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
View File
@@ -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")
+64
View File
@@ -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
```
+2 -2
View File
@@ -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
+12 -9
View File
@@ -6,10 +6,11 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1543955607869063269?label=discord)](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>
+17
View File
@@ -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,
+30
View File
@@ -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(
+38
View File
@@ -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_
+6
View File
@@ -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
};
+53
View File
@@ -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_
+23
View File
@@ -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_
+66
View File
@@ -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_
+7
View File
@@ -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
+1 -1
View File
@@ -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
-11
View File
@@ -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
+12
View File
@@ -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
+1
View File
@@ -16,6 +16,7 @@ depends=(
'libelf'
'libglvnd'
'libplacebo'
'libpng'
'libutf8proc'
'libyaml'
'minizip'
+1
View File
@@ -17,6 +17,7 @@ pkgbase = mocktail-bin
depends = libelf
depends = libglvnd
depends = libplacebo
depends = libpng
depends = libsoup3
depends = libutf8proc
depends = libyaml
+1
View File
@@ -20,6 +20,7 @@ depends=(
'libelf'
'libglvnd'
'libplacebo'
'libpng'
'libsoup3'
'libutf8proc'
'libyaml'
+1
View File
@@ -24,6 +24,7 @@ pkgbase = mocktail-git
depends = libelf
depends = libglvnd
depends = libplacebo
depends = libpng
depends = libsoup3
depends = libutf8proc
depends = libyaml
+1
View File
@@ -20,6 +20,7 @@ depends=(
'libelf'
'libglvnd'
'libplacebo'
'libpng'
'libsoup3'
'libutf8proc'
'libyaml'
+1
View File
@@ -24,6 +24,7 @@ pkgbase = mocktail
depends = libelf
depends = libglvnd
depends = libplacebo
depends = libpng
depends = libsoup3
depends = libutf8proc
depends = libyaml
+1
View File
@@ -20,6 +20,7 @@ depends=(
'libelf'
'libglvnd'
'libplacebo'
'libpng'
'libsoup3'
'libutf8proc'
'libyaml'
+2
View File
@@ -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
+14 -10
View File
@@ -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
+2
View File
@@ -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)"
+2 -1
View File
@@ -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,
+7 -4
View File
@@ -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 \
+1
View File
@@ -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)
]
+125 -30
View File
@@ -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
+3
View File
@@ -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'
+8 -2
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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) {
+167 -71
View File
@@ -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, &parameters);
}
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);
}
+11
View File
@@ -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;
+28
View File
@@ -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",
+19 -10
View File
@@ -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;
}
+456 -5
View File
@@ -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"
+398
View File
@@ -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
+182
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+4 -9
View File
@@ -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 -3
View File
@@ -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 | "
+35
View File
@@ -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
+9 -2
View File
@@ -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;
+18 -3
View File
@@ -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() {
+59
View File
@@ -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
+11 -5
View File
@@ -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
View File
@@ -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},
+4 -2
View File
@@ -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;
+2 -1
View File
@@ -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=")
+13
View File
@@ -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") ||
+5 -3
View File
@@ -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() ||
+6 -3
View File
@@ -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 =
+7
View File
@@ -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
)
+6
View File
@@ -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
+245
View File
@@ -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'
+6 -2
View File
@@ -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()));
+18 -14
View File
@@ -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, &parameters), 0);
ASSERT_EQ(
mocktail_pthread_attr_setschedparam(&attributes, &parameters), 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);
}
}
+6 -2
View File
@@ -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);
+28
View File
@@ -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" &&
+6
View File
@@ -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
View File
@@ -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;
+71
View File
@@ -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
+11 -8
View File
@@ -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;
+16 -5
View File
@@ -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"));
}