mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 13:00:45 +02:00
Migrated from GitLab
This commit is contained in:
commit
4e78c7d8b3
4279 files changed
+2025790
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# Preserve exact upstream whitespace in the provenance-locked Android core
|
||||
# snapshot. Local Mocktail sources remain subject to git diff --check.
|
||||
third_party/mcpelauncher-linker/core/** -whitespace
|
||||
@@ -0,0 +1,155 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
contracts:
|
||||
name: Repository contracts
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Validate shell and JSON files
|
||||
shell: bash
|
||||
run: |
|
||||
find scripts tests -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||
find config packaging/flatpak -type f -name '*.json' -print0 \
|
||||
| xargs -0 -n1 python3 -m json.tool >/dev/null
|
||||
|
||||
- name: Verify build and packaging contracts
|
||||
shell: bash
|
||||
run: |
|
||||
bash tests/managed_payload_build_contract_test.sh .
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak repository and bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
publish=false
|
||||
if [[ "$GITHUB_REF" == refs/heads/main && \
|
||||
"$GITHUB_EVENT_NAME" != pull_request ]]; then
|
||||
publish=true
|
||||
[[ -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 [[ "$publish" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
if [[ "$publish" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg public
|
||||
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
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
@@ -0,0 +1,255 @@
|
||||
name: Native packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: packages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: AppImage (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
build-essential cmake git ninja-build pkg-config \
|
||||
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 zlib1g-dev \
|
||||
libplacebo-dev \
|
||||
bubblewrap xdg-dbus-proxy patchelf jq unzip file curl \
|
||||
ca-certificates default-jdk-headless libgif7 python3 \
|
||||
python3-capstone python3-yaml python3-requests \
|
||||
python3-urllib3 python3-certifi python3-charset-normalizer \
|
||||
python3-idna squashfs-tools
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
|
||||
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
install -m 0755 /tmp/appimagetool /usr/local/bin/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/android-build-tools.zip \
|
||||
https://dl.google.com/android/repository/build-tools_r35_linux.zip
|
||||
echo "bd3a4966912eb8b30ed0d00b0cda6b6543b949d5ffe00bea54c04c81e1561d88 /tmp/android-build-tools.zip" | \
|
||||
sha256sum --check --strict
|
||||
unzip -q /tmp/android-build-tools.zip -d /tmp/android-build-tools
|
||||
android_tools_root="$(dirname -- "$(find /tmp/android-build-tools -type f -name apksigner -print -quit)")"
|
||||
[[ -n "$android_tools_root" ]]
|
||||
|
||||
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 APPIMAGE_EXTRACT_AND_RUN=1
|
||||
export MOCKTAIL_ANDROID_BUILD_TOOLS_ROOT="$android_tools_root"
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone
|
||||
test -x dist/Mocktail-x86_64.AppImage
|
||||
'
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
build-essential cmake git ninja-build pkg-config fakeroot \
|
||||
dpkg-dev file libsdl3-dev libsdl3-ttf-dev \
|
||||
libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \
|
||||
libyaml-dev libelf-dev libminizip-dev libcapstone-dev \
|
||||
libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev \
|
||||
libvulkan-dev libplacebo-dev zlib1g-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
-G DEB -B dist/deb
|
||||
dpkg-deb --info dist/deb/*.deb >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-DEB
|
||||
path: dist/deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
rpm:
|
||||
name: RPM (Fedora 44)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config rpm-build \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \
|
||||
webkitgtk6.0-devel utf8proc-devel fontconfig-devel \
|
||||
libglvnd-devel vulkan-headers vulkan-loader-devel \
|
||||
libplacebo-devel zlib-ng-compat-devel
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
-G RPM -B dist/rpm
|
||||
rpm --query --package --info dist/rpm/*.rpm >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-RPM
|
||||
path: dist/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
arch:
|
||||
name: Pacman package (Arch Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build pacman package
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
pacman -Syu --needed --noconfirm \
|
||||
cmake git ninja pkgconf 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
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
makepkg --dir /workspace/packaging/arch \
|
||||
--noconfirm --cleanbuild --force
|
||||
install -d dist/arch
|
||||
cp packaging/arch/*.pkg.tar.zst dist/arch/
|
||||
pacman -Qp dist/arch/*.pkg.tar.zst >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload pacman package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-Arch
|
||||
path: dist/arch/*.pkg.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish continuous packages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Mocktail-*
|
||||
path: release
|
||||
merge-multiple: true
|
||||
|
||||
- name: Update continuous release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
find release -maxdepth 1 -type f -print
|
||||
git config user.name komaruworld
|
||||
git config user.email rtysus228@gmail.com
|
||||
git tag --force continuous "$GITHUB_SHA"
|
||||
git push origin refs/tags/continuous --force
|
||||
if ! gh release view continuous >/dev/null 2>&1; then
|
||||
gh release create continuous \
|
||||
--prerelease \
|
||||
--title "Mocktail continuous" \
|
||||
--notes "Automated development packages from main."
|
||||
fi
|
||||
gh release upload continuous release/* --clobber
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
# Build
|
||||
/build/
|
||||
/build_*/
|
||||
/build-flatpak/
|
||||
/.flatpak-builder/
|
||||
/cmake-build-*/
|
||||
/out/
|
||||
/dist/
|
||||
/bin/
|
||||
/lib/
|
||||
/CMakeFiles/
|
||||
CMakeCache.txt
|
||||
CMakeScripts/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
DartConfiguration.tcl
|
||||
Testing/
|
||||
CMakeUserPresets.json
|
||||
*.cmake.user
|
||||
|
||||
*.o
|
||||
*.obj
|
||||
*.a
|
||||
*.lib
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
*.dll
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.d
|
||||
*.lo
|
||||
*.la
|
||||
*.mod
|
||||
*.smod
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
*.make
|
||||
*.ninja
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
Makefile.cmake
|
||||
|
||||
/_deps/
|
||||
/cmake-build-debug/
|
||||
/cmake-build-release/
|
||||
|
||||
# Roblox payload
|
||||
/rbx_bin/libroblox.so
|
||||
/rbx_bin/libroblox.so.*
|
||||
/rbx_bin/client_settings_android.json
|
||||
/rbx_bin/assets/
|
||||
/rbx_bin/android/
|
||||
/rbx_bin/sober_apk/
|
||||
/rbx_bin/versions/
|
||||
/rbx_bin/.payload-update.*/
|
||||
/rbx_bin/*.apk
|
||||
/rbx_bin/*.so
|
||||
/rbx_bin/*.so.*
|
||||
|
||||
!/rbx_bin/.gitkeep
|
||||
|
||||
# Runtime state
|
||||
/runtime/
|
||||
/appData/
|
||||
/cache/
|
||||
/assets/
|
||||
/ContentProvider_*/
|
||||
/http/
|
||||
/sounds/
|
||||
/tmp/
|
||||
/temp/
|
||||
|
||||
# Logs
|
||||
/logs/
|
||||
/*.log
|
||||
/run_*.log
|
||||
/build_err.txt
|
||||
/relocations.txt
|
||||
/undefined_symbols.txt
|
||||
/base-error.zip
|
||||
*.log
|
||||
*.core
|
||||
core
|
||||
core.*
|
||||
vgcore.*
|
||||
|
||||
# Android core subset used by the linker
|
||||
!/third_party/mcpelauncher-linker/core/
|
||||
/third_party/mcpelauncher-linker/core/*
|
||||
!/third_party/mcpelauncher-linker/core/MODULE_LICENSE_APACHE2
|
||||
!/third_party/mcpelauncher-linker/core/NOTICE
|
||||
!/third_party/mcpelauncher-linker/core/base/
|
||||
!/third_party/mcpelauncher-linker/core/libcutils/
|
||||
!/third_party/mcpelauncher-linker/core/liblog/
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/
|
||||
|
||||
# Reverse-engineering output
|
||||
*.i64
|
||||
*.id0
|
||||
*.id1
|
||||
*.id2
|
||||
*.nam
|
||||
*.til
|
||||
*.idb
|
||||
*.lcf
|
||||
*.ghidra
|
||||
*.gpr
|
||||
*.rep
|
||||
*.r2
|
||||
*.r2db
|
||||
*.rzdb
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
.venv/
|
||||
venv/
|
||||
.env/
|
||||
*.egg
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.tox/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Local configuration
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pem
|
||||
!exe/cacert.pem
|
||||
cookies*.txt
|
||||
*credentials*
|
||||
*secret*
|
||||
*token*.json
|
||||
|
||||
# Coverage and profiling
|
||||
*.gcno
|
||||
*.gcda
|
||||
*.gcov
|
||||
coverage/
|
||||
*.profraw
|
||||
*.profdata
|
||||
asan.log*
|
||||
tsan.log*
|
||||
ubsan.log*
|
||||
lsan.log*
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
*.7z
|
||||
*.rar
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*.apk
|
||||
*.aab
|
||||
*.xapk
|
||||
*.obb
|
||||
*.dump
|
||||
*.mem
|
||||
*.raw
|
||||
|
||||
# Test fixtures
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.zip
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.apk
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "third_party/libjnivm"]
|
||||
path = third_party/libjnivm
|
||||
url = https://github.com/ChristopherHX/libjnivm.git
|
||||
[submodule "third_party/Vulkan-Headers"]
|
||||
path = third_party/Vulkan-Headers
|
||||
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||
+677
@@ -0,0 +1,677 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 0.1.0
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# libjnivm uses C++ exceptions, so these cannot be directory-wide.
|
||||
add_library(mocktail_compile_options INTERFACE)
|
||||
target_compile_options(mocktail_compile_options INTERFACE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-Wall;-Wextra;-Wpedantic;-Wno-unused-parameter;-fno-exceptions;-g>"
|
||||
)
|
||||
|
||||
function(mocktail_apply_compile_options target_name)
|
||||
target_link_libraries(${target_name} PRIVATE mocktail_compile_options)
|
||||
endfunction()
|
||||
|
||||
option(MOCKTAIL_ENABLE_UPSTREAM_JNIVM
|
||||
"Build upstream libjnivm for the incremental JNI runtime migration" ON)
|
||||
if(MOCKTAIL_ENABLE_UPSTREAM_JNIVM)
|
||||
set(JNIVM_ENABLE_TRACE OFF CACHE BOOL
|
||||
"Enable tracing inside upstream libjnivm")
|
||||
set(JNIVM_ENABLE_GC ON CACHE BOOL
|
||||
"Enable local/global reference tracking inside upstream libjnivm")
|
||||
set(JNIVM_ENABLE_DEBUG OFF CACHE BOOL
|
||||
"Enable upstream libjnivm class-dump/code-generation support")
|
||||
set(JNIVM_ENABLE_RETURN_NON_ZERO OFF CACHE BOOL
|
||||
"Allow upstream libjnivm to fabricate unresolved object return values")
|
||||
set(JNIVM_FAKE_JNI_MINECRAFT_LINUX_COMPAT OFF CACHE BOOL
|
||||
"Enable legacy fake-jni static method promotion")
|
||||
set(JNIVM_ENABLE_TESTS OFF CACHE BOOL
|
||||
"Build upstream libjnivm's own test suite")
|
||||
set(JNIVM_BUILD_EXAMPLES OFF CACHE BOOL
|
||||
"Build upstream libjnivm examples")
|
||||
|
||||
add_subdirectory(third_party/libjnivm EXCLUDE_FROM_ALL)
|
||||
# Work around a missing include in libjnivm.
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(jnivm PRIVATE -include string)
|
||||
endif()
|
||||
target_link_libraries(jnivm PUBLIC Threads::Threads)
|
||||
add_library(Mocktail::UpstreamJni ALIAS jnivm)
|
||||
endif()
|
||||
|
||||
add_subdirectory(third_party/mcpelauncher-linker)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
target_compile_options(linker PRIVATE -w)
|
||||
endif()
|
||||
|
||||
set(VULKAN_HEADERS_ENABLE_TESTS OFF CACHE BOOL
|
||||
"Build Khronos Vulkan-Headers tests" FORCE)
|
||||
set(VULKAN_HEADERS_ENABLE_INSTALL OFF CACHE BOOL
|
||||
"Install vendored Vulkan-Headers" FORCE)
|
||||
add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
# Package builds disable tests and do not fetch Google Test.
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0
|
||||
)
|
||||
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
endif()
|
||||
|
||||
add_library(mocktail_crypto_foundation STATIC
|
||||
src/foundation/sha256.cc
|
||||
)
|
||||
add_library(Mocktail::CryptoFoundation ALIAS mocktail_crypto_foundation)
|
||||
target_include_directories(mocktail_crypto_foundation PUBLIC include)
|
||||
mocktail_apply_compile_options(mocktail_crypto_foundation)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailPlatformGraphics.cmake)
|
||||
foreach(foundation_target IN ITEMS
|
||||
mocktail_platform_sdl
|
||||
mocktail_graphics_foundation
|
||||
mocktail_sdl_vulkan_wsi)
|
||||
mocktail_apply_compile_options(${foundation_target})
|
||||
endforeach()
|
||||
foreach(graphics_test_target IN ITEMS
|
||||
bionic_egl_bridge_test
|
||||
platform_graphics_foundation_test)
|
||||
if(TARGET ${graphics_test_target})
|
||||
mocktail_apply_compile_options(${graphics_test_target})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailAudio.cmake)
|
||||
foreach(audio_target IN ITEMS
|
||||
mocktail_audio_core
|
||||
mocktail_audio_sdl
|
||||
mocktail_audio_fmod_java_runtime
|
||||
mocktail_fmod_jni_audio_bridge
|
||||
mocktail_audio_opensl_adapter
|
||||
mocktail_opensles)
|
||||
mocktail_apply_compile_options(${audio_target})
|
||||
endforeach()
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16)
|
||||
# GCC 16 reports a false array-bounds warning in this translation unit.
|
||||
target_compile_options(mocktail_audio_opensl_adapter PRIVATE
|
||||
-Wno-array-bounds
|
||||
)
|
||||
endif()
|
||||
if(TARGET audio_foundation_test)
|
||||
mocktail_apply_compile_options(audio_foundation_test)
|
||||
endif()
|
||||
if(TARGET opensl_playback_runtime_test)
|
||||
mocktail_apply_compile_options(opensl_playback_runtime_test)
|
||||
endif()
|
||||
if(TARGET fmod_java_audio_runtime_test)
|
||||
mocktail_apply_compile_options(fmod_java_audio_runtime_test)
|
||||
endif()
|
||||
if(TARGET fmod_jni_audio_bridge_test)
|
||||
mocktail_apply_compile_options(fmod_jni_audio_bridge_test)
|
||||
endif()
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailElfCompatibility.cmake)
|
||||
|
||||
# Bionic stdio symbols must exist before the linker target.
|
||||
add_library(mocktail_bionic_stdio_runtime STATIC
|
||||
src/compat/bionic_stdio_runtime.cc
|
||||
)
|
||||
add_library(Mocktail::BionicStdio ALIAS mocktail_bionic_stdio_runtime)
|
||||
target_include_directories(mocktail_bionic_stdio_runtime PUBLIC include)
|
||||
mocktail_apply_compile_options(mocktail_bionic_stdio_runtime)
|
||||
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBYAML REQUIRED IMPORTED_TARGET yaml-0.1)
|
||||
pkg_check_modules(MINIZIP REQUIRED IMPORTED_TARGET minizip)
|
||||
pkg_check_modules(CAPSTONE REQUIRED IMPORTED_TARGET capstone)
|
||||
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
|
||||
pkg_check_modules(LIBADWAITA REQUIRED IMPORTED_TARGET "libadwaita-1>=1.6")
|
||||
pkg_check_modules(WEBKITGTK6 REQUIRED IMPORTED_TARGET webkitgtk-6.0)
|
||||
|
||||
add_library(mocktail_update STATIC
|
||||
src/update/android_manifest.cc
|
||||
src/update/apk_bundle.cc
|
||||
src/update/apk_signature.cc
|
||||
src/update/apkpure_provider.cc
|
||||
src/update/candidate_approval.cc
|
||||
src/update/compatibility_catalog.cc
|
||||
src/update/http_download.cc
|
||||
src/update/host_abi_deriver.cc
|
||||
src/update/payload_integrity.cc
|
||||
src/update/payload_store.cc
|
||||
src/update/readiness_canary.cc
|
||||
src/update/update_config.cc
|
||||
src/update/update_coordinator.cc
|
||||
src/update/zip_archive.cc
|
||||
)
|
||||
add_library(Mocktail::Update ALIAS mocktail_update)
|
||||
target_include_directories(mocktail_update PUBLIC include)
|
||||
target_link_libraries(mocktail_update PUBLIC
|
||||
Mocktail::Compat
|
||||
Mocktail::CryptoFoundation
|
||||
CURL::libcurl
|
||||
OpenSSL::Crypto
|
||||
PkgConfig::LIBYAML
|
||||
PkgConfig::MINIZIP
|
||||
PkgConfig::CAPSTONE
|
||||
nlohmann_json::nlohmann_json
|
||||
Threads::Threads
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_update)
|
||||
|
||||
add_executable(mocktail_updater src/update/main.cc)
|
||||
target_link_libraries(mocktail_updater PRIVATE Mocktail::Update)
|
||||
target_compile_definitions(mocktail_updater PRIVATE
|
||||
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${CMAKE_SOURCE_DIR}/config/roblox_compatibility.json\""
|
||||
"MOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=\"${CMAKE_SOURCE_DIR}/config/roblox_signing_certificates.json\""
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_updater)
|
||||
|
||||
add_library(mocktail_legacy_jnivm STATIC
|
||||
src/jnivm/jnivm.cc
|
||||
)
|
||||
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
|
||||
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
|
||||
mocktail_apply_compile_options(mocktail_legacy_jnivm)
|
||||
# A JDK is not required for these ABI-only headers.
|
||||
set(MOCKTAIL_JNI_INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/third_party/jni/include
|
||||
)
|
||||
target_include_directories(mocktail_legacy_jnivm PUBLIC
|
||||
${MOCKTAIL_JNI_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
add_library(mocktail_runtime STATIC
|
||||
src/runtime/command_line.cc
|
||||
src/runtime/crash_report_policy.cc
|
||||
src/runtime/device_profile.cc
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
src/runtime/game_mode.cc
|
||||
src/runtime/game_session_coordinator.cc
|
||||
src/runtime/graphics_launch_policy.cc
|
||||
src/runtime/http_client_policy.cc
|
||||
src/runtime/memory_limit.cc
|
||||
src/runtime/owned_pthread.cc
|
||||
src/runtime/performance_policy.cc
|
||||
src/runtime/platform_cache_migration.cc
|
||||
src/runtime/roblox_desktop_app_policy.cc
|
||||
src/runtime/roblox_app_lifecycle.cc
|
||||
src/runtime/roblox_browser_service_bridge.cc
|
||||
src/runtime/roblox_capability_resolver.cc
|
||||
src/runtime/roblox_experience_launch_bridge.cc
|
||||
src/runtime/roblox_launch_uri.cc
|
||||
src/runtime/roblox_game_session_native_adapter.cc
|
||||
src/runtime/roblox_web_view_bridge.cc
|
||||
src/runtime/payload_update_preflight.cc
|
||||
src/runtime/runtime_config.cc
|
||||
src/runtime/runtime_config_bootstrap.cc
|
||||
src/runtime/runtime_config_file.cc
|
||||
src/runtime/runtime_paths.cc
|
||||
src/runtime/single_instance_lock.cc
|
||||
src/runtime/startup_state_machine.cc
|
||||
src/runtime/supported_launch_policy.cc
|
||||
src/runtime/support_bundle.cc
|
||||
)
|
||||
add_library(Mocktail::Runtime ALIAS mocktail_runtime)
|
||||
target_include_directories(mocktail_runtime PUBLIC include)
|
||||
target_include_directories(mocktail_runtime PUBLIC
|
||||
${MOCKTAIL_JNI_INCLUDE_DIRS}
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_runtime)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16)
|
||||
# GCC 16 reports a false array-bounds warning in nlohmann::json::dump().
|
||||
set_property(SOURCE src/runtime/roblox_launch_uri.cc APPEND PROPERTY
|
||||
COMPILE_OPTIONS -Wno-array-bounds
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
|
||||
target_link_libraries(mocktail_runtime PRIVATE
|
||||
Mocktail::CryptoFoundation
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
PkgConfig::LIBYAML
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
|
||||
# Keep GTK out of the process that loads Android libraries.
|
||||
add_library(mocktail_failure_dialog_runtime STATIC
|
||||
src/runtime/failure_dialog.cc
|
||||
)
|
||||
add_library(Mocktail::FailureDialog ALIAS mocktail_failure_dialog_runtime)
|
||||
target_include_directories(mocktail_failure_dialog_runtime PUBLIC include)
|
||||
target_compile_definitions(mocktail_failure_dialog_runtime PRIVATE
|
||||
"MOCKTAIL_INSTALL_LIBDIR=\"${CMAKE_INSTALL_LIBDIR}\""
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_failure_dialog_runtime)
|
||||
|
||||
add_executable(mocktail_failure_dialog
|
||||
src/ui/mocktail_failure_dialog.cc
|
||||
)
|
||||
target_link_libraries(mocktail_failure_dialog PRIVATE PkgConfig::LIBADWAITA)
|
||||
mocktail_apply_compile_options(mocktail_failure_dialog)
|
||||
|
||||
add_library(mocktail_services STATIC
|
||||
src/services/http_client.cc
|
||||
src/services/auth_service.cc
|
||||
src/services/browser_tracker_service.cc
|
||||
src/services/client_settings_service.cc
|
||||
)
|
||||
add_library(Mocktail::Services ALIAS mocktail_services)
|
||||
target_include_directories(mocktail_services PUBLIC include)
|
||||
target_link_libraries(mocktail_services PUBLIC
|
||||
mocktail_runtime
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_services)
|
||||
|
||||
add_library(mocktail_webview_launcher STATIC
|
||||
src/runtime/webview_helper_launcher.cc
|
||||
)
|
||||
add_library(Mocktail::WebViewLauncher ALIAS mocktail_webview_launcher)
|
||||
target_include_directories(mocktail_webview_launcher PUBLIC include)
|
||||
target_link_libraries(mocktail_webview_launcher PUBLIC CURL::libcurl)
|
||||
mocktail_apply_compile_options(mocktail_webview_launcher)
|
||||
|
||||
add_library(mocktail_webview_helper_policy STATIC
|
||||
src/webview/webview_helper_policy.cc
|
||||
)
|
||||
add_library(Mocktail::WebViewHelperPolicy ALIAS mocktail_webview_helper_policy)
|
||||
target_include_directories(mocktail_webview_helper_policy PUBLIC src)
|
||||
target_link_libraries(mocktail_webview_helper_policy PUBLIC
|
||||
PkgConfig::WEBKITGTK6
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_webview_helper_policy)
|
||||
|
||||
add_executable(mocktail_webview_helper
|
||||
src/webview/mocktail_webview_helper.cc
|
||||
)
|
||||
target_link_libraries(mocktail_webview_helper PRIVATE
|
||||
Mocktail::WebViewLauncher
|
||||
Mocktail::WebViewHelperPolicy
|
||||
Mocktail::Runtime
|
||||
PkgConfig::GTK4
|
||||
PkgConfig::WEBKITGTK6
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_webview_helper)
|
||||
|
||||
add_library(mocktail_auth_runtime_composition STATIC
|
||||
src/runtime/auth_runtime_composition.cc
|
||||
src/runtime/webview_roblox_cookie.cc
|
||||
)
|
||||
add_library(Mocktail::AuthRuntimeComposition ALIAS
|
||||
mocktail_auth_runtime_composition)
|
||||
target_include_directories(mocktail_auth_runtime_composition PUBLIC include)
|
||||
target_link_libraries(mocktail_auth_runtime_composition PUBLIC
|
||||
mocktail_legacy_jnivm
|
||||
mocktail_runtime
|
||||
mocktail_services
|
||||
)
|
||||
target_link_libraries(mocktail_auth_runtime_composition PRIVATE
|
||||
Mocktail::CryptoFoundation
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_auth_runtime_composition)
|
||||
|
||||
add_library(mocktail_linker STATIC
|
||||
src/linker/linker.cc
|
||||
)
|
||||
target_include_directories(mocktail_linker PUBLIC include)
|
||||
target_link_libraries(mocktail_linker PUBLIC
|
||||
dl
|
||||
linker
|
||||
mocktail_compat
|
||||
mocktail_bionic_stdio_runtime
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_linker)
|
||||
target_compile_definitions(mocktail_linker PUBLIC MOCKTAIL_USE_BIONIC_LINKER=1)
|
||||
# The Bionic linker expects IPLT boundaries that host executables do not have.
|
||||
target_link_options(mocktail_linker INTERFACE
|
||||
-Wl,--defsym=__rela_iplt_start=0
|
||||
-Wl,--defsym=__rela_iplt_end=0
|
||||
)
|
||||
|
||||
add_library(libc_shim STATIC
|
||||
src/libc_shim/libc_shim.cc
|
||||
)
|
||||
target_include_directories(libc_shim PUBLIC include)
|
||||
mocktail_apply_compile_options(libc_shim)
|
||||
target_link_libraries(mocktail_linker PUBLIC libc_shim)
|
||||
|
||||
add_subdirectory(stubs)
|
||||
foreach(stub_target IN ITEMS
|
||||
stub_sys_m stub_sys_z stub_sys_dl stub_sys_c
|
||||
stub_android stub_log stub_mediandk stub_openmax
|
||||
stub_egl stub_glesv2 stub_vulkan)
|
||||
mocktail_apply_compile_options(${stub_target})
|
||||
endforeach()
|
||||
|
||||
add_library(mocktail_window STATIC
|
||||
src/window/roblox_fullscreen_menu_request_gate.cc
|
||||
src/window/video_driver_policy.cc
|
||||
src/window/window.cc
|
||||
src/window/window_fullscreen_request_gate.cc
|
||||
src/window/window_game_surface_bridge.cc
|
||||
src/window/window_pointer_capture_owner.cc
|
||||
src/window/window_resize_readiness_gate.cc
|
||||
src/window/window_state_store.cc
|
||||
src/window/window_surface_lifecycle.cc
|
||||
src/window/window_text_input_owner.cc
|
||||
)
|
||||
target_include_directories(mocktail_window PUBLIC src)
|
||||
mocktail_apply_compile_options(mocktail_window)
|
||||
target_link_libraries(mocktail_window PUBLIC
|
||||
Mocktail::PlatformSdl
|
||||
SDL3::SDL3
|
||||
${CMAKE_DL_LIBS}
|
||||
PRIVATE Mocktail::Runtime
|
||||
)
|
||||
|
||||
add_library(mocktail_roblox_output_device_runtime STATIC
|
||||
src/audio/roblox_output_device_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_roblox_output_device_runtime PUBLIC include)
|
||||
target_include_directories(mocktail_roblox_output_device_runtime PRIVATE src)
|
||||
target_link_libraries(mocktail_roblox_output_device_runtime PUBLIC
|
||||
mocktail_compat
|
||||
Mocktail::AudioSdl
|
||||
PRIVATE mocktail_linker
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_roblox_output_device_runtime)
|
||||
|
||||
add_library(mocktail_roblox_fullscreen_runtime STATIC
|
||||
src/runtime/roblox_fullscreen_runtime_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_roblox_fullscreen_runtime PUBLIC include)
|
||||
target_include_directories(mocktail_roblox_fullscreen_runtime PRIVATE src)
|
||||
target_link_libraries(mocktail_roblox_fullscreen_runtime PUBLIC
|
||||
mocktail_compat
|
||||
PRIVATE
|
||||
mocktail_linker
|
||||
mocktail_window
|
||||
stub_log
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_roblox_fullscreen_runtime)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailInput.cmake)
|
||||
|
||||
add_library(mocktail_experience_runtime STATIC
|
||||
src/runtime/jnivm_platform_web_callbacks.cc
|
||||
src/runtime/roblox_experience_composition.cc
|
||||
src/runtime/roblox_fresh_game_launch_controller.cc
|
||||
src/runtime/roblox_platform_web_symbols.cc
|
||||
)
|
||||
add_library(Mocktail::ExperienceRuntime ALIAS mocktail_experience_runtime)
|
||||
target_include_directories(mocktail_experience_runtime PUBLIC include)
|
||||
target_include_directories(mocktail_experience_runtime PRIVATE src)
|
||||
target_link_libraries(mocktail_experience_runtime PUBLIC
|
||||
Mocktail::Runtime
|
||||
Mocktail::WebViewLauncher
|
||||
PkgConfig::UTF8PROC
|
||||
PRIVATE
|
||||
Mocktail::AuthRuntimeComposition
|
||||
Mocktail::LegacyJni
|
||||
mocktail_linker
|
||||
mocktail_window
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_experience_runtime)
|
||||
|
||||
# Compatibility exports still used by the old startup path.
|
||||
add_library(mocktail_bionic_abi_exports STATIC
|
||||
src/compat/bionic_abi_exports.cc
|
||||
src/compat/http_client_spin_guard.cc
|
||||
)
|
||||
target_include_directories(mocktail_bionic_abi_exports
|
||||
PUBLIC include
|
||||
PRIVATE src
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_bionic_abi_exports)
|
||||
target_link_libraries(mocktail_bionic_abi_exports PRIVATE
|
||||
mocktail_bionic_stdio_runtime
|
||||
mocktail_window
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
include(CheckIncludeFileCXX)
|
||||
include(CMakePushCheckState)
|
||||
|
||||
check_include_file_cxx(execinfo.h MOCKTAIL_HAVE_EXECINFO_H)
|
||||
if(NOT MOCKTAIL_HAVE_EXECINFO_H)
|
||||
message(FATAL_ERROR
|
||||
"execinfo.h is required for crash diagnostics; on musl install "
|
||||
"libexecinfo-devel"
|
||||
)
|
||||
endif()
|
||||
|
||||
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "")
|
||||
set(_mocktail_execinfo_link_probe [=[
|
||||
#include <execinfo.h>
|
||||
int main() {
|
||||
void* frames[1] = {};
|
||||
const int count = backtrace(frames, 1);
|
||||
return backtrace_symbols(frames, count) == nullptr;
|
||||
}
|
||||
]=])
|
||||
check_cxx_source_compiles(
|
||||
"${_mocktail_execinfo_link_probe}"
|
||||
MOCKTAIL_EXECINFO_IN_HOST_LIBC
|
||||
)
|
||||
if(NOT MOCKTAIL_EXECINFO_IN_HOST_LIBC)
|
||||
find_library(MOCKTAIL_EXECINFO_LIBRARY NAMES execinfo REQUIRED)
|
||||
cmake_push_check_state(RESET)
|
||||
set(CMAKE_REQUIRED_LIBRARIES "${MOCKTAIL_EXECINFO_LIBRARY}")
|
||||
check_cxx_source_compiles(
|
||||
"${_mocktail_execinfo_link_probe}"
|
||||
MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY
|
||||
)
|
||||
cmake_pop_check_state()
|
||||
if(NOT MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY)
|
||||
message(FATAL_ERROR
|
||||
"execinfo.h is available, but backtrace and backtrace_symbols cannot "
|
||||
"be linked"
|
||||
)
|
||||
endif()
|
||||
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "${MOCKTAIL_EXECINFO_LIBRARY}")
|
||||
endif()
|
||||
unset(_mocktail_execinfo_link_probe)
|
||||
|
||||
add_library(mocktail_legacy_runtime STATIC
|
||||
src/legacy/legacy_runtime.cc
|
||||
src/runtime/roblox_text_input_jni_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_legacy_runtime PRIVATE include src)
|
||||
mocktail_apply_compile_options(mocktail_legacy_runtime)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# Diagnostics from signal handlers cannot handle a failed write.
|
||||
set_property(SOURCE src/legacy/legacy_runtime.cc APPEND PROPERTY
|
||||
COMPILE_OPTIONS -Wno-unused-result
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(mocktail_legacy_runtime PRIVATE
|
||||
mocktail_legacy_jnivm
|
||||
mocktail_compat
|
||||
mocktail_runtime
|
||||
mocktail_input_runtime
|
||||
mocktail_experience_runtime
|
||||
mocktail_services
|
||||
mocktail_linker
|
||||
mocktail_bionic_abi_exports
|
||||
libc_shim
|
||||
mocktail_window
|
||||
mocktail_graphics_foundation
|
||||
${MOCKTAIL_EXECINFO_LINK_LIBRARY}
|
||||
)
|
||||
target_compile_definitions(mocktail_legacy_runtime PRIVATE
|
||||
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${CMAKE_SOURCE_DIR}/config/roblox_compatibility.json\""
|
||||
)
|
||||
|
||||
add_executable(mocktail src/main.cc)
|
||||
target_include_directories(mocktail PRIVATE include src)
|
||||
mocktail_apply_compile_options(mocktail)
|
||||
target_compile_definitions(mocktail PRIVATE
|
||||
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${CMAKE_SOURCE_DIR}/config/roblox_compatibility.json\""
|
||||
)
|
||||
target_link_libraries(mocktail PRIVATE
|
||||
mocktail_legacy_runtime
|
||||
mocktail_auth_runtime_composition
|
||||
mocktail_failure_dialog_runtime
|
||||
mocktail_fmod_jni_audio_bridge
|
||||
mocktail_roblox_output_device_runtime
|
||||
mocktail_roblox_fullscreen_runtime
|
||||
mocktail_window
|
||||
libc_shim
|
||||
)
|
||||
|
||||
# Libraries loaded with dlopen() need the stub symbols.
|
||||
target_link_options(mocktail PRIVATE
|
||||
-Wl,--export-dynamic
|
||||
)
|
||||
|
||||
set_target_properties(mocktail PROPERTIES
|
||||
BUILD_RPATH "\$ORIGIN"
|
||||
INSTALL_RPATH "\$ORIGIN;\$ORIGIN/../${CMAKE_INSTALL_LIBDIR}/mocktail"
|
||||
)
|
||||
|
||||
set(MOCKTAIL_INSTALL_RUNTIME_DIR "${CMAKE_INSTALL_LIBDIR}/mocktail")
|
||||
set(MOCKTAIL_INSTALL_DATA_DIR "${CMAKE_INSTALL_DATADIR}/mocktail")
|
||||
|
||||
install(TARGETS mocktail
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
|
||||
)
|
||||
install(TARGETS
|
||||
mocktail
|
||||
mocktail_updater
|
||||
mocktail_failure_dialog
|
||||
mocktail_webview_helper
|
||||
mocktail_audio_sdl
|
||||
mocktail_opensles
|
||||
stub_sys_m
|
||||
stub_sys_z
|
||||
stub_sys_dl
|
||||
stub_sys_c
|
||||
stub_android
|
||||
stub_log
|
||||
stub_mediandk
|
||||
stub_openmax
|
||||
stub_egl
|
||||
stub_glesv2
|
||||
stub_vulkan
|
||||
RUNTIME DESTINATION "${MOCKTAIL_INSTALL_RUNTIME_DIR}"
|
||||
LIBRARY DESTINATION "${MOCKTAIL_INSTALL_RUNTIME_DIR}"
|
||||
)
|
||||
install(FILES
|
||||
config/roblox_compatibility.json
|
||||
config/roblox_bootstrap_sources.json
|
||||
config/roblox_host_abi_reference.json
|
||||
config/roblox_signing_certificates.json
|
||||
DESTINATION "${MOCKTAIL_INSTALL_DATA_DIR}/metadata"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.desktop
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.metainfo.xml
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.svg
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
|
||||
)
|
||||
set(MOCKTAIL_HICOLOR_ICON_SIZES
|
||||
16 22 24 32 36 48 64 72 96 128 192 256 512
|
||||
)
|
||||
# Generated icons are committed, so ImageMagick is not a build dependency.
|
||||
foreach(icon_size IN LISTS MOCKTAIL_HICOLOR_ICON_SIZES)
|
||||
install(FILES
|
||||
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/space.bigrat.mocktail.png"
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_DATADIR}/icons/hicolor/${icon_size}x${icon_size}/apps"
|
||||
)
|
||||
endforeach()
|
||||
install(CODE "
|
||||
if(NOT DEFINED ENV{DESTDIR} OR \"\$ENV{DESTDIR}\" STREQUAL \"\")
|
||||
find_program(MOCKTAIL_GTK_UPDATE_ICON_CACHE gtk-update-icon-cache)
|
||||
if(MOCKTAIL_GTK_UPDATE_ICON_CACHE)
|
||||
execute_process(
|
||||
COMMAND \"\${MOCKTAIL_GTK_UPDATE_ICON_CACHE}\" --force
|
||||
--ignore-theme-index
|
||||
\"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/icons/hicolor\"
|
||||
RESULT_VARIABLE MOCKTAIL_ICON_CACHE_RESULT
|
||||
)
|
||||
if(NOT MOCKTAIL_ICON_CACHE_RESULT EQUAL 0)
|
||||
message(WARNING \"Failed to refresh the hicolor icon cache\")
|
||||
endif()
|
||||
endif()
|
||||
find_program(MOCKTAIL_UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||||
if(MOCKTAIL_UPDATE_DESKTOP_DATABASE)
|
||||
execute_process(
|
||||
COMMAND \"\${MOCKTAIL_UPDATE_DESKTOP_DATABASE}\" -q
|
||||
\"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications\"
|
||||
RESULT_VARIABLE MOCKTAIL_DESKTOP_DATABASE_RESULT
|
||||
)
|
||||
if(NOT MOCKTAIL_DESKTOP_DATABASE_RESULT EQUAL 0)
|
||||
message(WARNING \"Failed to refresh the desktop MIME cache\")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
")
|
||||
|
||||
set(CPACK_PACKAGE_NAME "mocktail")
|
||||
set(CPACK_PACKAGE_VENDOR "Mocktail Project")
|
||||
set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION
|
||||
"Runs the Android x86-64 Roblox client on Linux."
|
||||
)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
|
||||
set(CPACK_STRIP_FILES TRUE)
|
||||
|
||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
|
||||
set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
|
||||
include(CPack)
|
||||
|
||||
add_dependencies(mocktail all_stubs)
|
||||
add_dependencies(mocktail mocktail_failure_dialog)
|
||||
add_dependencies(mocktail mocktail_webview_helper)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,165 @@
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch install-updater payload-status payload-rollback support-bundle clean submodules help
|
||||
|
||||
BUILD_DIR := build
|
||||
BINARY := $(BUILD_DIR)/mocktail
|
||||
JOBS := $(shell nproc)
|
||||
BUILD_TYPE ?= Release
|
||||
LIBC ?= auto
|
||||
MODE ?= standalone
|
||||
CMAKE_TOOLCHAIN_FILE ?=
|
||||
CMAKE_SYSROOT ?=
|
||||
RELEASE_BUILD_DIR ?=
|
||||
PORTABLE_MODE ?= standalone
|
||||
PORTABLE_CANONICAL_MODE := $(if $(filter dynamic minimal,$(PORTABLE_MODE)),thin,$(if $(filter full static,$(PORTABLE_MODE)),standalone,$(PORTABLE_MODE)))
|
||||
PORTABLE_DIR ?= dist/mocktail-linux-x86_64-glibc-$(PORTABLE_CANONICAL_MODE)
|
||||
APPIMAGE ?= dist/Mocktail-x86_64.AppImage
|
||||
PREFIX ?= /usr
|
||||
FLATPAK_BUILD_DIR ?= build-flatpak
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/space.bigrat.mocktail.json
|
||||
FLATPAK_JOBS ?= 4
|
||||
|
||||
define build_native_runtime
|
||||
@git submodule update --init --recursive
|
||||
@cmake -S . -B "$(BUILD_DIR)" \
|
||||
-DCMAKE_BUILD_TYPE="$(1)" \
|
||||
-DBUILD_TESTING="$(2)" \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
$(if $(strip $(CMAKE_TOOLCHAIN_FILE)),-DCMAKE_TOOLCHAIN_FILE="$(CMAKE_TOOLCHAIN_FILE)") \
|
||||
$(if $(strip $(CMAKE_SYSROOT)),-DCMAKE_SYSROOT="$(CMAKE_SYSROOT)")
|
||||
@cmake --build "$(BUILD_DIR)" -j"$(JOBS)"
|
||||
endef
|
||||
|
||||
all: build ## Dynamic Release build
|
||||
|
||||
build: ## Build the dynamic Release runtime in build/
|
||||
$(call build_native_runtime,$(BUILD_TYPE),OFF)
|
||||
|
||||
release: ## Build AppImage (LIBC=auto|glibc|musl MODE=standalone|thin)
|
||||
@MOCKTAIL_BUILD_JOBS="$(JOBS)" \
|
||||
MOCKTAIL_BUILD_TYPE="$(BUILD_TYPE)" \
|
||||
MOCKTAIL_RELEASE_BUILD_DIR="$(RELEASE_BUILD_DIR)" \
|
||||
CMAKE_TOOLCHAIN_FILE="$(CMAKE_TOOLCHAIN_FILE)" \
|
||||
CMAKE_SYSROOT="$(CMAKE_SYSROOT)" \
|
||||
./scripts/build_release.sh --libc "$(LIBC)" --mode "$(MODE)"
|
||||
|
||||
debug: ## Debug build
|
||||
$(call build_native_runtime,Debug,OFF)
|
||||
|
||||
install: ## Install an already-built dynamic runtime (use: sudo make install)
|
||||
@cmake --install "$(BUILD_DIR)" --prefix "$(PREFIX)"
|
||||
|
||||
register-url-handler: build ## Select this build for Roblox website links
|
||||
@./scripts/register_url_handler.sh --set-default \
|
||||
--executable "$(abspath $(BINARY))" \
|
||||
--desktop-file "$(abspath packaging/space.bigrat.mocktail.desktop)"
|
||||
|
||||
test: ## Build and run all unit tests
|
||||
$(call build_native_runtime,Debug,ON)
|
||||
@ctest --test-dir "$(BUILD_DIR)" --output-on-failure -j"$(JOBS)"
|
||||
|
||||
apk: ## Extract libroblox.so and build (usage: make apk APK=/path/to/roblox.apk)
|
||||
ifndef APK
|
||||
$(error APK variable not set. Usage: make apk APK=/path/to/roblox-x86_64.apk)
|
||||
endif
|
||||
@./scripts/build.sh --apk "$(APK)" --build-type $(BUILD_TYPE) --jobs $(JOBS)
|
||||
|
||||
release-runtime: build
|
||||
|
||||
portable: appimage ## Build the portable AppImage
|
||||
|
||||
portable-test: release-runtime ## Verify portable packaging and relocation
|
||||
@./tests/portable_packaging_test.sh
|
||||
|
||||
standalone: appimage ## Build the standalone x86-64 AppImage
|
||||
|
||||
appimage: release-runtime ## Build AppImage (requires appimagetool)
|
||||
@command -v appimagetool >/dev/null 2>&1 || { \
|
||||
echo "appimagetool is required for make appimage" >&2; \
|
||||
echo "Install it, then rerun make appimage." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@./scripts/package_portable.sh --build-dir "$(BUILD_DIR)" \
|
||||
--libc glibc --output "$(PORTABLE_DIR)" --mode "$(PORTABLE_MODE)" \
|
||||
--appimage "$(APPIMAGE)"
|
||||
|
||||
flatpak: ## Build and install the local x86-64 Flatpak
|
||||
@./scripts/build_flatpak.sh \
|
||||
--build-dir "$(FLATPAK_BUILD_DIR)" \
|
||||
--manifest "$(FLATPAK_MANIFEST)" \
|
||||
--jobs "$(FLATPAK_JOBS)"
|
||||
|
||||
run-flatpak: ## Run the installed Flatpak
|
||||
@flatpak run space.bigrat.mocktail
|
||||
|
||||
run: export MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS = 0
|
||||
run: build ## Run the native binary interactively until the window is closed
|
||||
@$(BINARY)
|
||||
|
||||
run-smoke: export MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS = 3000
|
||||
run-smoke: release-runtime ## Run the short Vulkan lifecycle readiness gate
|
||||
@./scripts/real_bringup_smoke.sh C
|
||||
|
||||
run-unlimited: export MOCKTAIL_FRAME_RATE_LIMIT = unlimited
|
||||
run-unlimited: export MOCKTAIL_VSYNC = off
|
||||
run-unlimited: export MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS = 5000
|
||||
run-unlimited: release-runtime ## Run 240-FPS/unthrottled Vulkan diagnostic mode
|
||||
@./scripts/real_bringup_smoke.sh C
|
||||
|
||||
run-game: release-runtime ## Run local UGCGame/Vulkan gate (not an authenticated join)
|
||||
@./scripts/real_bringup_smoke.sh GAME
|
||||
|
||||
run-input: release-runtime ## Run local GAME gate and route SDL mouse/touch through native input
|
||||
@./scripts/real_bringup_smoke.sh INPUT
|
||||
|
||||
run-resize: release-runtime ## Run real SDL compositor resize and typed JNI surface-rebind gate
|
||||
@./scripts/real_bringup_smoke.sh RESIZE
|
||||
|
||||
run-network: export MOCKTAIL_PLACE_ID = $(PLACE_ID)
|
||||
run-network: release-runtime ## Run authenticated public-place gate (usage: make run-network PLACE_ID=...)
|
||||
ifndef PLACE_ID
|
||||
$(error PLACE_ID variable not set. Usage: make run-network PLACE_ID=positive-place-id)
|
||||
endif
|
||||
@./scripts/real_bringup_smoke.sh NETWORK
|
||||
|
||||
auto-run: ## Build/run loop with crash summaries
|
||||
@./scripts/auto_runtime_loop.sh
|
||||
|
||||
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
|
||||
|
||||
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
|
||||
@./scripts/update_roblox_payload.sh
|
||||
|
||||
update-auto: build ## Download latest Roblox, derive its ABI, canary twice and activate safely
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
|
||||
update-auto-launch: build ## Update Roblox and launch the verified current payload
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
@./$(BINARY)
|
||||
|
||||
install-updater: ## Install and enable the six-hour systemd user update timer
|
||||
@./scripts/install_user_updater.sh
|
||||
|
||||
payload-status: build ## Show current and previous-good immutable payloads
|
||||
@./$(BUILD_DIR)/mocktail_updater status
|
||||
|
||||
payload-rollback: build ## Atomically restore the previous-good payload
|
||||
@./$(BUILD_DIR)/mocktail_updater rollback
|
||||
|
||||
support-bundle: ## Create a private sanitized diagnostics archive
|
||||
@./scripts/collect_support_bundle.sh --context manual --reason manual-request
|
||||
|
||||
submodules: ## Initialise git submodules (libjnivm, mcpelauncher-linker)
|
||||
@./scripts/add_submodules.sh
|
||||
|
||||
clean: ## Remove the build directory
|
||||
rm -rf $(BUILD_DIR)
|
||||
@echo "Build directory removed."
|
||||
|
||||
help: ## Show available targets
|
||||
@grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) \
|
||||
| awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -0,0 +1,106 @@
|
||||
# Mocktail
|
||||
|
||||
[](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
|
||||
[](https://github.com/komaruworld/mocktail/stargazers)
|
||||
[](https://github.com/komaruworld/mocktail/releases/tag/continuous)
|
||||
[](LICENSE)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux. It provides the
|
||||
Android ABI and JNI pieces the client expects, then uses SDL3 and Vulkan or
|
||||
OpenGL for the Linux side.
|
||||
|
||||
Mocktail is still experimental and may break after a Roblox update. It is not
|
||||
affiliated with Roblox Corporation or VinegarHQ.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
|
||||
```
|
||||
|
||||
The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Mocktail. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user https://komaruworld.github.io/mocktail/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
## Other Linux packages
|
||||
|
||||
The latest AppImage, DEB, RPM, and Arch packages are attached to the
|
||||
[continuous release](https://github.com/komaruworld/mocktail/releases/tag/continuous).
|
||||
|
||||
```bash
|
||||
# AppImage
|
||||
chmod +x Mocktail-x86_64.AppImage
|
||||
./Mocktail-x86_64.AppImage
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install ./mocktail_*_amd64.deb
|
||||
|
||||
# Fedora
|
||||
sudo dnf install ./mocktail-*.x86_64.rpm
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -U ./mocktail-*-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Only Linux `x86_64` is supported. Building requires CMake 3.20+, Git,
|
||||
pkg-config, 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.
|
||||
|
||||
<details>
|
||||
<summary>Ubuntu 26.04+</summary>
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential cmake git ninja-build pkg-config \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev zlib1g-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Arch Linux</summary>
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed base-devel cmake git ninja pkgconf sdl3 sdl3_ttf \
|
||||
curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
libplacebo vulkan-headers vulkan-icd-loader zlib
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora 44+</summary>
|
||||
|
||||
```bash
|
||||
sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel minizip-ng-compat-devel \
|
||||
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
|
||||
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
|
||||
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel
|
||||
```
|
||||
</details>
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
|
||||
cd mocktail
|
||||
make build
|
||||
./build/mocktail
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE). Third-party components keep their own licenses.
|
||||
@@ -0,0 +1,208 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(MOCKTAIL_AUDIO_ROOT
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE
|
||||
)
|
||||
|
||||
# SDL 3.4 provides the callback needed for OpenSL buffer completion.
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(mocktail_audio_core STATIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/audio_sink.cc
|
||||
)
|
||||
target_include_directories(mocktail_audio_core PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_compile_features(mocktail_audio_core PUBLIC cxx_std_17)
|
||||
set_target_properties(mocktail_audio_core PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
add_library(Mocktail::AudioCore ALIAS mocktail_audio_core)
|
||||
|
||||
add_library(mocktail_audio_sdl SHARED
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/sdl_audio_sink.cc
|
||||
)
|
||||
target_include_directories(mocktail_audio_sdl PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_audio_sdl PUBLIC
|
||||
Mocktail::AudioCore
|
||||
SDL3::SDL3
|
||||
)
|
||||
target_compile_features(mocktail_audio_sdl PUBLIC cxx_std_17)
|
||||
set_target_properties(mocktail_audio_sdl PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
BUILD_RPATH_USE_ORIGIN TRUE
|
||||
BUILD_RPATH "\$ORIGIN"
|
||||
INSTALL_RPATH "\$ORIGIN"
|
||||
)
|
||||
add_library(Mocktail::AudioSdl ALIAS mocktail_audio_sdl)
|
||||
|
||||
add_library(mocktail_audio_fmod_java_runtime STATIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/fmod_java_audio_runtime.cc
|
||||
)
|
||||
target_include_directories(mocktail_audio_fmod_java_runtime PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_audio_fmod_java_runtime PUBLIC
|
||||
Mocktail::AudioSdl
|
||||
Threads::Threads
|
||||
)
|
||||
target_compile_features(mocktail_audio_fmod_java_runtime PUBLIC cxx_std_17)
|
||||
set_target_properties(mocktail_audio_fmod_java_runtime PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
add_library(Mocktail::FmodJavaAudioRuntime ALIAS
|
||||
mocktail_audio_fmod_java_runtime
|
||||
)
|
||||
|
||||
add_library(mocktail_fmod_jni_audio_bridge STATIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/fmod_jni_audio_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_fmod_jni_audio_bridge PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_fmod_jni_audio_bridge PUBLIC
|
||||
Mocktail::FmodJavaAudioRuntime
|
||||
Mocktail::LegacyJni
|
||||
)
|
||||
target_compile_features(mocktail_fmod_jni_audio_bridge PUBLIC cxx_std_17)
|
||||
set_target_properties(mocktail_fmod_jni_audio_bridge PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
add_library(Mocktail::FmodJniAudioBridge ALIAS
|
||||
mocktail_fmod_jni_audio_bridge
|
||||
)
|
||||
|
||||
add_library(mocktail_audio_opensl_adapter STATIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/opensl_simple_buffer_queue.cc
|
||||
)
|
||||
target_include_directories(mocktail_audio_opensl_adapter PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_audio_opensl_adapter PUBLIC
|
||||
Mocktail::AudioCore
|
||||
Threads::Threads
|
||||
)
|
||||
target_compile_features(mocktail_audio_opensl_adapter PUBLIC cxx_std_17)
|
||||
set_target_properties(mocktail_audio_opensl_adapter PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
add_library(Mocktail::AudioOpenSlAdapter ALIAS
|
||||
mocktail_audio_opensl_adapter
|
||||
)
|
||||
|
||||
add_library(mocktail_audio INTERFACE)
|
||||
target_link_libraries(mocktail_audio INTERFACE
|
||||
Mocktail::AudioSdl
|
||||
Mocktail::AudioOpenSlAdapter
|
||||
)
|
||||
add_library(Mocktail::Audio ALIAS mocktail_audio)
|
||||
|
||||
# Desktop builds use opensl_abi.h when NDK headers are unavailable.
|
||||
find_path(MOCKTAIL_OPENSL_CORE_INCLUDE_DIR SLES/OpenSLES.h)
|
||||
find_path(MOCKTAIL_OPENSL_ANDROID_INCLUDE_DIR SLES/OpenSLES_Android.h)
|
||||
if(MOCKTAIL_OPENSL_CORE_INCLUDE_DIR AND MOCKTAIL_OPENSL_ANDROID_INCLUDE_DIR)
|
||||
target_include_directories(mocktail_audio_opensl_adapter PUBLIC
|
||||
${MOCKTAIL_OPENSL_CORE_INCLUDE_DIR}
|
||||
${MOCKTAIL_OPENSL_ANDROID_INCLUDE_DIR}
|
||||
)
|
||||
target_compile_definitions(mocktail_audio_opensl_adapter PUBLIC
|
||||
MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS=1
|
||||
)
|
||||
message(STATUS "Mocktail audio: using system Android OpenSL headers")
|
||||
else()
|
||||
message(STATUS
|
||||
"Mocktail audio: Android OpenSL headers unavailable; using minimal queue ABI boundary"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(mocktail_opensles SHARED
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/opensl_playback_runtime.cc
|
||||
)
|
||||
target_include_directories(mocktail_opensles PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_opensles PRIVATE
|
||||
Mocktail::Audio
|
||||
)
|
||||
target_compile_features(mocktail_opensles PRIVATE cxx_std_17)
|
||||
set_target_properties(mocktail_opensles PROPERTIES
|
||||
OUTPUT_NAME OpenSLES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN YES
|
||||
BUILD_RPATH_USE_ORIGIN TRUE
|
||||
BUILD_RPATH "\$ORIGIN"
|
||||
INSTALL_RPATH "\$ORIGIN"
|
||||
)
|
||||
target_link_options(mocktail_opensles PRIVATE
|
||||
"-Wl,-soname,libOpenSLES.so"
|
||||
"-Wl,--exclude-libs,ALL"
|
||||
)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(audio_foundation_test
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/audio_foundation_test.cc
|
||||
${MOCKTAIL_AUDIO_ROOT}/stubs/libopensl_stub.cc
|
||||
)
|
||||
target_link_libraries(audio_foundation_test PRIVATE
|
||||
Mocktail::Audio
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_compile_features(audio_foundation_test PRIVATE cxx_std_17)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(audio_foundation_test
|
||||
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
|
||||
)
|
||||
|
||||
add_executable(opensl_playback_runtime_test
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/opensl_playback_runtime_test.cc
|
||||
)
|
||||
target_link_libraries(opensl_playback_runtime_test PRIVATE
|
||||
mocktail_opensles
|
||||
Mocktail::AudioSdl
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_compile_features(opensl_playback_runtime_test PRIVATE cxx_std_17)
|
||||
gtest_discover_tests(opensl_playback_runtime_test
|
||||
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
|
||||
)
|
||||
|
||||
add_executable(fmod_java_audio_runtime_test
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/fmod_java_audio_runtime_test.cc
|
||||
)
|
||||
target_link_libraries(fmod_java_audio_runtime_test PRIVATE
|
||||
Mocktail::FmodJavaAudioRuntime
|
||||
GTest::gtest_main
|
||||
Threads::Threads
|
||||
)
|
||||
target_compile_features(fmod_java_audio_runtime_test PRIVATE cxx_std_17)
|
||||
gtest_discover_tests(fmod_java_audio_runtime_test
|
||||
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
|
||||
)
|
||||
|
||||
add_executable(fmod_jni_audio_bridge_test
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/fmod_jni_audio_bridge_test.cc
|
||||
)
|
||||
target_link_libraries(fmod_jni_audio_bridge_test PRIVATE
|
||||
Mocktail::FmodJniAudioBridge
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_compile_features(fmod_jni_audio_bridge_test PRIVATE cxx_std_17)
|
||||
gtest_discover_tests(fmod_jni_audio_bridge_test
|
||||
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
|
||||
)
|
||||
|
||||
add_test(NAME audio_readiness_gate_test
|
||||
COMMAND bash
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/audio_readiness_gate_test.sh
|
||||
${MOCKTAIL_AUDIO_ROOT}/scripts/audio_readiness_gate.sh
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(MOCKTAIL_ELF_COMPAT_ROOT
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE
|
||||
)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBELF REQUIRED IMPORTED_TARGET libelf)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
add_library(mocktail_compat STATIC
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_atfork_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_dns_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_host_libc_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_large_file_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_rwlock_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_signal_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_semaphore_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_pthread_create_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_pthread_key_runtime.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_sysconf.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/elf_build_id.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/build_profile.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/payload_compatibility.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/host_allocator_bridge.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/host_abi_experiment.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/host_abi_profile.cc
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/host_abi_profile_loader.cc
|
||||
)
|
||||
add_library(Mocktail::Compat ALIAS mocktail_compat)
|
||||
target_include_directories(mocktail_compat PUBLIC
|
||||
${MOCKTAIL_ELF_COMPAT_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_compat PUBLIC
|
||||
PkgConfig::LIBELF
|
||||
nlohmann_json::nlohmann_json
|
||||
Threads::Threads
|
||||
)
|
||||
target_link_libraries(mocktail_compat PRIVATE Mocktail::CryptoFoundation)
|
||||
target_compile_features(mocktail_compat PUBLIC cxx_std_17)
|
||||
|
||||
if(COMMAND mocktail_apply_compile_options)
|
||||
mocktail_apply_compile_options(mocktail_compat)
|
||||
endif()
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(UTF8PROC REQUIRED IMPORTED_TARGET libutf8proc)
|
||||
pkg_check_modules(SDL3_TTF REQUIRED IMPORTED_TARGET sdl3-ttf)
|
||||
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
|
||||
|
||||
get_filename_component(MOCKTAIL_INPUT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.."
|
||||
ABSOLUTE
|
||||
)
|
||||
|
||||
add_library(mocktail_input_runtime STATIC
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_native_adapter.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_router.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_native_text_box_info_reader.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_display_state.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_editor.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_surface_overlay.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_window_input_runtime.cc
|
||||
)
|
||||
add_library(Mocktail::InputRuntime ALIAS mocktail_input_runtime)
|
||||
target_include_directories(mocktail_input_runtime PUBLIC
|
||||
${MOCKTAIL_INPUT_ROOT}/include
|
||||
)
|
||||
target_include_directories(mocktail_input_runtime PRIVATE
|
||||
${MOCKTAIL_INPUT_ROOT}/src
|
||||
)
|
||||
target_link_libraries(mocktail_input_runtime PUBLIC
|
||||
Mocktail::PlatformSdl
|
||||
Mocktail::Runtime
|
||||
mocktail_window
|
||||
PkgConfig::UTF8PROC
|
||||
PRIVATE
|
||||
PkgConfig::FONTCONFIG
|
||||
PkgConfig::SDL3_TTF
|
||||
)
|
||||
target_compile_features(mocktail_input_runtime PUBLIC cxx_std_17)
|
||||
mocktail_apply_compile_options(mocktail_input_runtime)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(roblox_input_router_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_router_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_input_router_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_input_router_test)
|
||||
|
||||
add_executable(roblox_text_editor_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_text_editor_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_text_editor_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_text_editor_test)
|
||||
|
||||
add_executable(roblox_text_display_state_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_text_display_state_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_text_display_state_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_text_display_state_test)
|
||||
|
||||
add_executable(roblox_input_native_adapter_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_native_adapter_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_input_native_adapter_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
Mocktail::LegacyJni
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_input_native_adapter_test)
|
||||
|
||||
add_executable(roblox_native_text_box_info_reader_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_native_text_box_info_reader_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_native_text_box_info_reader_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
Mocktail::LegacyJni
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_native_text_box_info_reader_test)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(roblox_input_router_test)
|
||||
gtest_discover_tests(roblox_text_editor_test)
|
||||
gtest_discover_tests(roblox_text_display_state_test)
|
||||
gtest_discover_tests(roblox_input_native_adapter_test)
|
||||
gtest_discover_tests(roblox_native_text_box_info_reader_test)
|
||||
add_test(NAME input_readiness_gate_synthetic
|
||||
COMMAND bash ${MOCKTAIL_INPUT_ROOT}/tests/input_readiness_gate_test.sh
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,159 @@
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(MOCKTAIL_PLATFORM_GRAPHICS_ROOT
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE
|
||||
)
|
||||
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_path(MOCKTAIL_EGL_INCLUDE_DIR EGL/egl.h REQUIRED)
|
||||
|
||||
set(MOCKTAIL_WINDOW_ICON_PNG
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/packaging/icons/hicolor/48x48/apps/space.bigrat.mocktail.png"
|
||||
)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${MOCKTAIL_WINDOW_ICON_PNG}"
|
||||
)
|
||||
file(READ "${MOCKTAIL_WINDOW_ICON_PNG}" MOCKTAIL_WINDOW_ICON_HEX HEX)
|
||||
string(LENGTH "${MOCKTAIL_WINDOW_ICON_HEX}" MOCKTAIL_WINDOW_ICON_HEX_LENGTH)
|
||||
set(MOCKTAIL_WINDOW_ICON_BYTES "")
|
||||
set(MOCKTAIL_WINDOW_ICON_HEX_OFFSET 0)
|
||||
set(MOCKTAIL_WINDOW_ICON_COLUMN 0)
|
||||
while(MOCKTAIL_WINDOW_ICON_HEX_OFFSET LESS MOCKTAIL_WINDOW_ICON_HEX_LENGTH)
|
||||
string(SUBSTRING "${MOCKTAIL_WINDOW_ICON_HEX}"
|
||||
${MOCKTAIL_WINDOW_ICON_HEX_OFFSET} 2 MOCKTAIL_WINDOW_ICON_BYTE
|
||||
)
|
||||
string(APPEND MOCKTAIL_WINDOW_ICON_BYTES
|
||||
"0x${MOCKTAIL_WINDOW_ICON_BYTE}, "
|
||||
)
|
||||
math(EXPR MOCKTAIL_WINDOW_ICON_HEX_OFFSET
|
||||
"${MOCKTAIL_WINDOW_ICON_HEX_OFFSET} + 2"
|
||||
)
|
||||
math(EXPR MOCKTAIL_WINDOW_ICON_COLUMN
|
||||
"${MOCKTAIL_WINDOW_ICON_COLUMN} + 1"
|
||||
)
|
||||
if(MOCKTAIL_WINDOW_ICON_COLUMN EQUAL 12)
|
||||
string(APPEND MOCKTAIL_WINDOW_ICON_BYTES "\n ")
|
||||
set(MOCKTAIL_WINDOW_ICON_COLUMN 0)
|
||||
endif()
|
||||
endwhile()
|
||||
|
||||
set(MOCKTAIL_PLATFORM_GENERATED_INCLUDE_DIR
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated"
|
||||
)
|
||||
file(MAKE_DIRECTORY
|
||||
"${MOCKTAIL_PLATFORM_GENERATED_INCLUDE_DIR}/mocktail/platform"
|
||||
)
|
||||
configure_file(
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/cmake/templates/sdl_window_icon_data.h.in"
|
||||
"${MOCKTAIL_PLATFORM_GENERATED_INCLUDE_DIR}/mocktail/platform/sdl_window_icon_data.h"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
set(MOCKTAIL_ANGLE_HEADERS_INCLUDE_DIR
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/third_party/angle_headers/include"
|
||||
)
|
||||
if(NOT EXISTS
|
||||
"${MOCKTAIL_ANGLE_HEADERS_INCLUDE_DIR}/EGL/eglext_angle.h")
|
||||
message(FATAL_ERROR "pinned ANGLE EGL extension header is unavailable")
|
||||
endif()
|
||||
add_library(mocktail_angle_headers INTERFACE)
|
||||
target_include_directories(mocktail_angle_headers SYSTEM INTERFACE
|
||||
"${MOCKTAIL_ANGLE_HEADERS_INCLUDE_DIR}"
|
||||
)
|
||||
add_library(Mocktail::AngleHeaders ALIAS mocktail_angle_headers)
|
||||
|
||||
add_library(mocktail_platform_sdl STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_application_metadata.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_display_refresh_capabilities.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_event_converter.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_platform_runtime.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_text_clipboard.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_window_icon.cc
|
||||
)
|
||||
target_include_directories(mocktail_platform_sdl PUBLIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/include
|
||||
)
|
||||
target_include_directories(mocktail_platform_sdl PRIVATE
|
||||
${MOCKTAIL_PLATFORM_GENERATED_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(mocktail_platform_sdl PUBLIC SDL3::SDL3)
|
||||
target_compile_features(mocktail_platform_sdl PUBLIC cxx_std_17)
|
||||
target_compile_definitions(mocktail_platform_sdl PRIVATE
|
||||
MOCKTAIL_PROJECT_VERSION="${PROJECT_VERSION}"
|
||||
)
|
||||
add_library(Mocktail::PlatformSdl ALIAS mocktail_platform_sdl)
|
||||
|
||||
add_library(mocktail_graphics_foundation STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/graphics_backend.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/angle_probe.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/bionic_egl_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_graphics_foundation
|
||||
PUBLIC ${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/include
|
||||
PRIVATE ${MOCKTAIL_EGL_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(mocktail_graphics_foundation PRIVATE
|
||||
Mocktail::AngleHeaders
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
target_compile_features(mocktail_graphics_foundation PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::GraphicsFoundation ALIAS mocktail_graphics_foundation)
|
||||
|
||||
add_library(mocktail_sdl_vulkan_wsi STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/sdl_vulkan_wsi.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/android_vulkan_wsi_adapter.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/present_mode_policy.cc
|
||||
)
|
||||
set_target_properties(mocktail_sdl_vulkan_wsi PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
target_include_directories(mocktail_sdl_vulkan_wsi PUBLIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/include
|
||||
)
|
||||
target_link_libraries(mocktail_sdl_vulkan_wsi PUBLIC SDL3::SDL3)
|
||||
target_link_libraries(mocktail_sdl_vulkan_wsi PUBLIC Vulkan::Headers)
|
||||
target_compile_features(mocktail_sdl_vulkan_wsi PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::SdlVulkanWsi ALIAS mocktail_sdl_vulkan_wsi)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(bionic_egl_bridge_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/bionic_egl_bridge_test.cc
|
||||
)
|
||||
target_link_libraries(bionic_egl_bridge_test PRIVATE
|
||||
Mocktail::GraphicsFoundation
|
||||
GTest::gtest_main
|
||||
)
|
||||
add_dependencies(bionic_egl_bridge_test stub_egl)
|
||||
|
||||
add_executable(platform_graphics_foundation_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/platform_graphics_foundation_test.cc
|
||||
)
|
||||
add_executable(display_refresh_capabilities_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/display_refresh_capabilities_test.cc
|
||||
)
|
||||
add_executable(present_mode_policy_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/present_mode_policy_test.cc
|
||||
)
|
||||
target_link_libraries(present_mode_policy_test PRIVATE
|
||||
Mocktail::SdlVulkanWsi
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_link_libraries(display_refresh_capabilities_test PRIVATE
|
||||
Mocktail::PlatformSdl
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_link_libraries(platform_graphics_foundation_test PRIVATE
|
||||
Mocktail::PlatformSdl
|
||||
Mocktail::GraphicsFoundation
|
||||
Mocktail::SdlVulkanWsi
|
||||
GTest::gtest_main
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(bionic_egl_bridge_test)
|
||||
gtest_discover_tests(platform_graphics_foundation_test)
|
||||
gtest_discover_tests(display_refresh_capabilities_test)
|
||||
gtest_discover_tests(present_mode_policy_test)
|
||||
endif()
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_DATA_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_DATA_H_
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
namespace internal {
|
||||
|
||||
inline constexpr unsigned char kMocktailWindowIconPng[] = {
|
||||
@MOCKTAIL_WINDOW_ICON_BYTES@
|
||||
};
|
||||
inline constexpr std::size_t kMocktailWindowIconPngSize =
|
||||
sizeof(kMocktailWindowIconPng);
|
||||
|
||||
} // namespace internal
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_DATA_H_
|
||||
@@ -0,0 +1,93 @@
|
||||
# Mocktail configuration.
|
||||
# The file is created once with mode 0600. Mocktail never replaces your edits.
|
||||
|
||||
# Integer: configuration schema version. Only version 1 is supported.
|
||||
version: 1
|
||||
|
||||
# One-line device preset. Short aliases pc, mobile, and console are accepted.
|
||||
# Presets: pc-windows-11, mobile-pixel-7, console-ps5.
|
||||
# Use the mapping below for a custom profile.
|
||||
device: pc-windows-11
|
||||
|
||||
# Detailed alternative (replace the scalar device line above):
|
||||
# device:
|
||||
# type: mobile
|
||||
# platform: Android
|
||||
# name: Google Pixel 9 Pro
|
||||
# manufacturer: Google
|
||||
# model: Pixel 9 Pro
|
||||
# brand: google
|
||||
# code: komodo
|
||||
# sku: pixel-9-pro
|
||||
# soc_model: Google Tensor G4
|
||||
# touch: true
|
||||
# mouse: false
|
||||
# keyboard: false
|
||||
|
||||
runtime:
|
||||
# Boolean (default: false): start without a visible SDL window.
|
||||
headless: false
|
||||
# String (optional): use a specific x86_64 libroblox.so instead of the
|
||||
# active verified payload.
|
||||
# roblox_library: /absolute/path/to/libroblox.so
|
||||
|
||||
graphics:
|
||||
# String (default: direct-vulkan): graphics backend. Use opengl on hardware
|
||||
# without a working Vulkan driver; OpenGL ES 3.0 or newer is required.
|
||||
# Recommended values: direct-vulkan, opengl, system, angle-vulkan.
|
||||
backend: direct-vulkan
|
||||
# String or integer (default: display): requested frame-rate policy.
|
||||
# Supported values: display, unlimited, 30, 60, 120, 144, 240. Unlimited
|
||||
# selects Roblox's unmodified 240-FPS scheduler maximum.
|
||||
frame_rate_limit: display
|
||||
# String (default: auto): presentation synchronization: auto, on, or off.
|
||||
vsync: auto
|
||||
|
||||
performance:
|
||||
# Boolean (default: false): size Roblox scheduler/render queues from every
|
||||
# physical CPU core. A place's Lua/main thread can still remain serial.
|
||||
multithreaded_rendering: false
|
||||
# String (default: throughput): `throughput` makes every physical core
|
||||
# available to Roblox worker pools; `latency` preserves Roblox-managed pool
|
||||
# sizes and coalesces midphase work. Supported: auto, latency, throughput.
|
||||
physics_worker_mode: throughput
|
||||
# Integer MiB (default: 0): hard RAM cap for the Mocktail process tree.
|
||||
# 0 disables the cap. cgroup v2 also disables swap; other Linux systems use
|
||||
# an RSS+swap watchdog. 6144 is a conservative starting point for 32 GiB RAM.
|
||||
memory_limit_mb: 0
|
||||
# String (default: auto): request Feral GameMode when its host daemon and
|
||||
# client library are available. Supported values: auto, on, off.
|
||||
gamemode: auto
|
||||
|
||||
audio:
|
||||
# String (default: default): `default` follows the host system output.
|
||||
# To pin output, copy an exact SDL device name printed during startup.
|
||||
output_device: default
|
||||
|
||||
window:
|
||||
# Integer (default: 1280): initial window width in pixels.
|
||||
width: 1280
|
||||
# Integer (default: 720): initial window height in pixels.
|
||||
height: 720
|
||||
# String (default: Roblox): window title.
|
||||
title: Roblox
|
||||
|
||||
# Optional host HTTP proxy. Uncomment both values together. The host must not
|
||||
# contain a scheme such as http://.
|
||||
# network:
|
||||
# # String (optional, default: disabled): proxy host name or IP address.
|
||||
# proxy_host: 127.0.0.1
|
||||
# # Integer (optional, default: disabled): proxy TCP port from 1 to 65535.
|
||||
# proxy_port: 8080
|
||||
|
||||
updates:
|
||||
# Boolean (default: true): check the latest Roblox version, derive an exact
|
||||
# HostAbi profile, run two isolated canaries with the selected graphics
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
# launches another process after changing the active payload.
|
||||
launch_after_update: false
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"sources": [
|
||||
{
|
||||
"package": "com.roblox.client",
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"abi": "x86_64",
|
||||
"provider": "uptodown",
|
||||
"page_url": "https://roblox.en.uptodown.com/android/download/1181025994-x",
|
||||
"file_id": "1181025994",
|
||||
"archive_size": 882739224,
|
||||
"archive_sha256": "7e32e419aee44dba19c46933241dd73a25d2ee7d51802f6e8a89d5dba7d76876"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"profiles": [
|
||||
{
|
||||
"version_name": "2.721.1108",
|
||||
"version_code": 2350,
|
||||
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
|
||||
"status": "legacy-researched",
|
||||
"default_allowed": false,
|
||||
"allow_legacy_binary_patches": true,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": false,
|
||||
"reason": "Reverse-engineered startup baseline; no verified real frame."
|
||||
},
|
||||
{
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"user_game_settings_fullscreen_setter_rva": "0x4095564",
|
||||
"fmod_output_device_bridge": {
|
||||
"vtable_rva": "0x6742678",
|
||||
"string_constructor_rva": "0x1bf898c",
|
||||
"count_method_rva": "0x2c2a082",
|
||||
"info_method_rva": "0x2c2a122",
|
||||
"current_method_rva": "0x2c2a0d2",
|
||||
"select_method_rva": "0x2c29e56"
|
||||
},
|
||||
"reason": "Verified 2026-07-10 on the current host: name-resolved ASMA/V2 startup, native Vulkan present, explicit lifecycle teardown, and Build-ID-scoped host output-device interposition without executable code patches or signal recovery; exact constructor replay remains Build-ID scoped."
|
||||
},
|
||||
{
|
||||
"version_name": "2.727.1199",
|
||||
"version_code": 2628,
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "Verified 2026-07-16 on the current host: independently signature-matched native allocator and pre-JNI state, 3479 constructors replayed without recovery, JNI_OnLoad and ASMA/V2 startup, 2988 Vulkan shaders, a real vkQueuePresentKHR, and explicit lifecycle teardown to Stopped; legacy executable patches and signal recovery remain disabled."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"payload_sha256": "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336997d",
|
||||
"payload_id": "2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"payload_path": "payloads/2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"reference": {
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"payload_sha256": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af"
|
||||
},
|
||||
"profile": {
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"bridge_entries": [
|
||||
{
|
||||
"rva": "0x1c39ea2",
|
||||
"kind": "allocate",
|
||||
"label": "small-allocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1fe4c84",
|
||||
"kind": "usable_size",
|
||||
"label": "usable-size"
|
||||
},
|
||||
{
|
||||
"rva": "0x203daae",
|
||||
"kind": "reallocate",
|
||||
"label": "reallocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c39c3a",
|
||||
"kind": "allocate",
|
||||
"label": "allocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c8dbf8",
|
||||
"kind": "aligned_allocate",
|
||||
"label": "aligned-allocate-direct"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c3eb72",
|
||||
"kind": "free",
|
||||
"label": "free"
|
||||
}
|
||||
],
|
||||
"data_seeds": {
|
||||
"allocator_object_slot": "0x7030ae0",
|
||||
"empty_string_slot": "0x703a7e8",
|
||||
"jni_singleton_slot": "0x72a16d0",
|
||||
"jni_singleton_bytes": 1024,
|
||||
"arena_initializer": "0x1c39f1d",
|
||||
"allocator_thread_initializer": "0x2040817",
|
||||
"arena_guard_slot": "0x71e92f0",
|
||||
"arena_table_slot": "0x71e9380",
|
||||
"arena_table_slot_count": 4194304
|
||||
},
|
||||
"native_allocator": {
|
||||
"allocate": "0x1c39c3a",
|
||||
"deallocate": "0x1c3eb72"
|
||||
},
|
||||
"init_array_offset": "0x6c2c9b0",
|
||||
"init_array_count": 3481,
|
||||
"constructor_run_ranges": [
|
||||
{
|
||||
"begin": 2,
|
||||
"end_exclusive": 3
|
||||
},
|
||||
{
|
||||
"begin": 5,
|
||||
"end_exclusive": 3481
|
||||
}
|
||||
],
|
||||
"native_mimalloc_constructor_run_ranges": [
|
||||
{
|
||||
"begin": 2,
|
||||
"end_exclusive": 3481
|
||||
}
|
||||
],
|
||||
"native_mimalloc_thread_initializer_after_constructor": 2,
|
||||
"native_pre_jni_bootstrap": {
|
||||
"registry_initializer": "0x1fe72e5",
|
||||
"registry_slot": "0x71ed428"
|
||||
},
|
||||
"default_allocator_strategy": "native_mimalloc"
|
||||
},
|
||||
"derivation_anchors": {
|
||||
"signature_version": 1,
|
||||
"allocator_object_initializer_rva": "0x1fbf1de",
|
||||
"empty_string_initializer_rva": "0x1fc82b0",
|
||||
"jni_singleton_initializer_rva": "0x1c39a34",
|
||||
"constructor_rvas": {
|
||||
"2": "0x1c3c764",
|
||||
"3": "0x1c38c00",
|
||||
"4": "0x1c39884",
|
||||
"5": "0x1c39a34"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"package": "com.roblox.client",
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"sha256": {
|
||||
"libroblox": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af",
|
||||
"base_apk": "eba476fec3020eaceb28436c1d160eb35d6689d8ffb060e09678535584babc7a",
|
||||
"x86_64_split_apk": "5c4193c9700481079cee958b38ba9cbb97768b0a130b7f704ed084753f01ce2b"
|
||||
},
|
||||
"signing_certificates_sha256": [
|
||||
"2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a",
|
||||
"44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477"
|
||||
],
|
||||
"assets": {
|
||||
"file_count": 1837
|
||||
},
|
||||
"source": "sober-update-service",
|
||||
"imported_at": "2026-07-09T19:12:54Z",
|
||||
"compatibility_status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"package": "com.roblox.client",
|
||||
"trusted_sha256": [
|
||||
"2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a",
|
||||
"44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Check, canary, and install the latest working Roblox for Mocktail
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=%h/.config/mocktail/updater.env
|
||||
ExecStart=/usr/bin/env ${MOCKTAIL_UPDATE_HELPER} update
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Periodically check Roblox updates for Mocktail
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5m
|
||||
OnUnitActiveSec=6h
|
||||
RandomizedDelaySec=15m
|
||||
Persistent=true
|
||||
Unit=mocktail-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"dependencies": {
|
||||
"libjnivm": {
|
||||
"mode": "git-submodule",
|
||||
"url": "https://github.com/ChristopherHX/libjnivm.git",
|
||||
"commit": "f24b98c198fcc5c59a68d1efadd3f5791eb01c2e",
|
||||
"tree": "77fa9e645f5b29ef4ac34a3fec8e36056f632e7b"
|
||||
},
|
||||
"OpenJDK-JNI-Headers": {
|
||||
"mode": "vendored-unmodified-header-snapshot",
|
||||
"url": "https://github.com/openjdk/jdk17u.git",
|
||||
"tag": "jdk-17.0.20+8",
|
||||
"upstream_commit": "8cbbca61432426a3441aa08838d930ef954ea1ba",
|
||||
"license": "GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"files": {
|
||||
"third_party/jni/include/jni.h": {
|
||||
"sha256": "1266aea5b9f5d5db1cb6f8e5c6c43cfa7f80bc4f72d7fe42c6131bb939dc70f4"
|
||||
},
|
||||
"third_party/jni/include/jni_md.h": {
|
||||
"sha256": "88cb5c33e306900dd35a78d5a439087123b8e91b0986bb5acb42cc9bd2fcc42e"
|
||||
},
|
||||
"third_party/jni/LICENSE": {
|
||||
"sha256": "4b9abebc4338048a7c2dc184e9f800deb349366bdf28eb23c2677a77b4c87726"
|
||||
},
|
||||
"third_party/jni/ASSEMBLY_EXCEPTION": {
|
||||
"sha256": "a44eb7b5caf5534c6ef536b21edb40b4d6babf91bf97d9d45596868618b2c6fb"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcpelauncher-linker": {
|
||||
"mode": "vendored-patched-snapshot",
|
||||
"url": "https://github.com/minecraft-linux/mcpelauncher-linker.git",
|
||||
"upstream_commit": "1ac3ea6c1cf4f73a84d73f0bff510fba94a7d0f2",
|
||||
"vendor_tree": "fca44d85e3773c135ccfc603b14abdf6aeb8efd1",
|
||||
"patch": "third_party/patches/mcpelauncher-linker.patch",
|
||||
"nested_sources": {
|
||||
"android_bionic": {
|
||||
"url": "https://github.com/minecraft-linux/android_bionic.git",
|
||||
"upstream_commit": "081b55b1f45745b1e6f93c49b3831107542a426d",
|
||||
"vendor_tree": "0b15096de36ed5d14becbf1fe06e2522bc85f5d7"
|
||||
},
|
||||
"android_core": {
|
||||
"mode": "vendored-unmodified-subtree-snapshot",
|
||||
"url": "https://github.com/minecraft-linux/android_core",
|
||||
"upstream_commit": "0235714fbf5593df145e8f991f82c5926c2df2df",
|
||||
"upstream_tree": "add6a2d8f9759489fcdd5787b6c001922d4c92b4",
|
||||
"vendor_tree": "b64d1a5874a602a09dcd6669b885ae1dc1a5021a",
|
||||
"vendored_subtrees": {
|
||||
"base": "1ebe3243f2a20d3b9b067a4806a20362abc0eefe",
|
||||
"libcutils": "e9543f04aeb288bc54430ee4824951d7cada541f",
|
||||
"liblog": "9e7341ce8c78a03e7e1c77b97a3c4e8c4c5153df",
|
||||
"libziparchive": "d63ea583b4a44461f373306d86cf8f67445bc042"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": [
|
||||
"The historical android_core submodule is vendored as four exact upstream subtrees plus its Apache license marker and NOTICE; unrelated core directories remain omitted.",
|
||||
"The patch records the C++ atomic compatibility layer, libc portability adapters, portable recursive locking, host C++ container reconstruction, the Linux PATH_MAX required by fortified realpath, and Mocktail constructor hooks."
|
||||
]
|
||||
},
|
||||
"ANGLE-Headers": {
|
||||
"mode": "vendored-unmodified-header-snapshot",
|
||||
"url": "https://chromium.googlesource.com/angle/angle.git",
|
||||
"upstream_commit": "384a29a8fa9ec59bd855e841d88993688cf833ef",
|
||||
"upstream_tree": "649d698943fc81203494904cc22a1d448a423c56",
|
||||
"files": {
|
||||
"include/EGL/eglext_angle.h": {
|
||||
"git_blob": "6184c866482e182f9e45b80d48746c58d671c3f0",
|
||||
"sha256": "df92523469632129b6b0c37bcade9fa8cc449e23e487beda2cc130b4e4bd1df5"
|
||||
},
|
||||
"LICENSE": {
|
||||
"git_blob": "0f65fd60fd68d465a2283378849f16532f5ee4f1",
|
||||
"sha256": "bf4da21bd20bcfb5b60b7ecc67fa864a79be049e21d6178076887f178dd6c71a"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Vulkan-Headers": {
|
||||
"mode": "git-submodule",
|
||||
"url": "https://github.com/KhronosGroup/Vulkan-Headers.git",
|
||||
"commit": "8d6039a455a7ecc7d2a592ff97f62db4e59b70bf"
|
||||
}
|
||||
}
|
||||
}
|
||||
+3576
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,97 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_ABI_EXPORTS_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_ABI_EXPORTS_H_
|
||||
|
||||
// Transitional ABI exports retained for the isolated legacy runtime. New
|
||||
// Android libraries must register owned symbols through the per-SONAME linker
|
||||
// API instead of adding process-global functions here.
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
#include "compat/bionic_stdio_runtime.h"
|
||||
|
||||
// Updated by the legacy loader after libroblox is mapped. The abort bridge
|
||||
// uses it only to report a caller-relative diagnostic offset.
|
||||
extern volatile uintptr_t g_mocktail_abort_libroblox_base;
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Enables diagnostics that contain offsets from the researched legacy Roblox
|
||||
// binary. The default is false and Build-ID policy is the only caller allowed
|
||||
// to enable it.
|
||||
void SetLegacyBionicDiagnosticsEnabled(bool enabled);
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
// Bionic x86-64 defines pthread_mutexattr_t as an eight-byte long. Keep that
|
||||
// guest layout explicit instead of exposing the incompatible host libc type.
|
||||
using MocktailBionicMutexAttr = int64_t;
|
||||
|
||||
extern "C" {
|
||||
|
||||
void mocktail_set_current_jni_env(void* env);
|
||||
void* mocktail_get_current_jni_env();
|
||||
|
||||
[[noreturn]] void mocktail_abort();
|
||||
int __system_property_get(const char* name, char* value);
|
||||
|
||||
void* __memcpy_chk(void* dst, const void* src, size_t count, size_t dst_len);
|
||||
char* __strcpy_chk(char* dst, const char* src, size_t dst_len);
|
||||
int __vsprintf_chk(char* dst, int flags, size_t dst_len, const char* format,
|
||||
va_list args);
|
||||
int __vsnprintf_chk(char* dst, size_t count, int flags, size_t dst_len,
|
||||
const char* format, va_list args);
|
||||
ssize_t __read_chk(int fd, void* buf, size_t count, size_t buf_len);
|
||||
|
||||
int mocktail_pthread_condattr_init(pthread_condattr_t* attr);
|
||||
int mocktail_pthread_condattr_destroy(pthread_condattr_t* attr);
|
||||
int mocktail_pthread_condattr_setclock(pthread_condattr_t* attr,
|
||||
clockid_t clock_id);
|
||||
int mocktail_pthread_cond_init(pthread_cond_t* cond,
|
||||
const pthread_condattr_t* attr);
|
||||
int mocktail_pthread_cond_destroy(pthread_cond_t* cond);
|
||||
int mocktail_pthread_cond_signal(pthread_cond_t* cond);
|
||||
int mocktail_pthread_cond_broadcast(pthread_cond_t* cond);
|
||||
int mocktail_pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_cond_timedwait(pthread_cond_t* cond,
|
||||
pthread_mutex_t* mutex,
|
||||
const timespec* abstime);
|
||||
int mocktail_pthread_mutexattr_init(MocktailBionicMutexAttr* attr);
|
||||
int mocktail_pthread_mutexattr_destroy(MocktailBionicMutexAttr* attr);
|
||||
int mocktail_pthread_mutexattr_settype(MocktailBionicMutexAttr* attr, int type);
|
||||
int mocktail_pthread_mutex_init(pthread_mutex_t* mutex,
|
||||
const MocktailBionicMutexAttr* attr);
|
||||
int mocktail_pthread_mutex_destroy(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_lock(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_trylock(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_unlock(pthread_mutex_t* mutex);
|
||||
|
||||
extern void* mocktail_gameactivity_on_start_native;
|
||||
extern void* mocktail_gameactivity_on_resume_native;
|
||||
extern void* mocktail_gameactivity_on_surface_created_native;
|
||||
extern void* mocktail_gameactivity_on_surface_changed_native;
|
||||
extern void* mocktail_gameactivity_on_surface_redraw_needed_native;
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_ABI_EXPORTS_H_
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_ATFORK_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_ATFORK_RUNTIME_H_
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
using BionicAtForkCallback = void (*)(void);
|
||||
|
||||
// Registers Android's __register_atfork callback contract with the host
|
||||
// pthread runtime. Bionic associates registrations with dso_handle and
|
||||
// removes them when that DSO unloads. POSIX pthread_atfork has no matching
|
||||
// unregister operation, so the host bridge accepts dso_handle for ABI
|
||||
// compatibility but cannot use it for cleanup.
|
||||
//
|
||||
// Callback code must remain mapped for every later fork. In practice, a guest
|
||||
// DSO that has registered callbacks must stay loaded until process exit;
|
||||
// unloading is safe only when the process guarantees that it will never fork
|
||||
// again.
|
||||
int RegisterBionicAtFork(BionicAtForkCallback prepare,
|
||||
BionicAtForkCallback parent,
|
||||
BionicAtForkCallback child, void* dso_handle) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Exact LP64 Android/Bionic __register_atfork signature. The prefixed host
|
||||
// symbol is exported to the guest as "__register_atfork" by synthetic libc.so.
|
||||
int mocktail_bionic_register_atfork(
|
||||
mocktail::compat::BionicAtForkCallback prepare,
|
||||
mocktail::compat::BionicAtForkCallback parent,
|
||||
mocktail::compat::BionicAtForkCallback child, void* dso_handle);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_ATFORK_RUNTIME_H_
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_DNS_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_DNS_RUNTIME_H_
|
||||
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace compat {
|
||||
|
||||
// Android x86-64 orders ai_canonname before ai_addr, unlike glibc. Keep the
|
||||
// guest layout explicit so DNS results never cross the ABI boundary as a host
|
||||
// addrinfo.
|
||||
struct BionicAddressInfo {
|
||||
int ai_flags = 0;
|
||||
int ai_family = 0;
|
||||
int ai_socktype = 0;
|
||||
int ai_protocol = 0;
|
||||
socklen_t ai_addrlen = 0;
|
||||
char* ai_canonname = nullptr;
|
||||
sockaddr* ai_addr = nullptr;
|
||||
BionicAddressInfo* ai_next = nullptr;
|
||||
};
|
||||
|
||||
constexpr int kBionicAddressInfoNameNotFound = 8;
|
||||
|
||||
// Matches only upload infrastructure embedded in the supported Roblox
|
||||
// payloads (plus Backtrace's public service suffix). Ordinary Roblox API,
|
||||
// asset, auth, and game-session hosts are outside this policy.
|
||||
bool IsBlockedCrashReportUploadHost(std::string_view host) noexcept;
|
||||
|
||||
int BionicGetAddressInfo(const char* node, const char* service,
|
||||
const BionicAddressInfo* hints,
|
||||
BionicAddressInfo** result) noexcept;
|
||||
void BionicFreeAddressInfo(BionicAddressInfo* info) noexcept;
|
||||
hostent* BionicGetHostByName(const char* name) noexcept;
|
||||
|
||||
} // namespace compat
|
||||
} // namespace mocktail
|
||||
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_getaddrinfo(
|
||||
const char* node, const char* service,
|
||||
const mocktail::compat::BionicAddressInfo* hints,
|
||||
mocktail::compat::BionicAddressInfo** result);
|
||||
void mocktail_bionic_freeaddrinfo(mocktail::compat::BionicAddressInfo* info);
|
||||
hostent* mocktail_bionic_gethostbyname(const char* name);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_DNS_RUNTIME_H_
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_HOST_LIBC_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_HOST_LIBC_RUNTIME_H_
|
||||
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android x86-64's malloc.h declares ten size_t fields in this order. This is
|
||||
// not the host's struct mallinfo: glibc's legacy type uses int,
|
||||
// and musl exposes no allocator-telemetry API. On hosts without a compatible
|
||||
// size_t-based telemetry API, BionicMallinfo returns an all-zero "unknown"
|
||||
// snapshot. Callers must not interpret that fallback as proof of zero usage.
|
||||
struct BionicMallinfoSnapshot {
|
||||
size_t arena = 0;
|
||||
size_t ordblks = 0;
|
||||
size_t smblks = 0;
|
||||
size_t hblks = 0;
|
||||
size_t hblkhd = 0;
|
||||
size_t usmblks = 0;
|
||||
size_t fsmblks = 0;
|
||||
size_t uordblks = 0;
|
||||
size_t fordblks = 0;
|
||||
size_t keepcost = 0;
|
||||
};
|
||||
|
||||
struct BionicLocaleState;
|
||||
using BionicLocale = BionicLocaleState*;
|
||||
using BionicThreadDestructor = void (*)(void*);
|
||||
|
||||
// Do not use the host socket structs here. Although glibc happens to match
|
||||
// Android LP64, musl declares cmsg_len and msg_controllen as 32-bit socklen_t.
|
||||
// Bionic x86-64 uses size_t for both, as audited from the vendored
|
||||
// bionic/libc/include/sys/socket.h.
|
||||
struct BionicMessageHeader {
|
||||
void* msg_name = nullptr;
|
||||
uint32_t msg_namelen = 0;
|
||||
iovec* msg_iov = nullptr;
|
||||
size_t msg_iovlen = 0;
|
||||
void* msg_control = nullptr;
|
||||
size_t msg_controllen = 0;
|
||||
int msg_flags = 0;
|
||||
};
|
||||
|
||||
struct BionicControlMessageHeader {
|
||||
size_t cmsg_len = 0;
|
||||
int cmsg_level = 0;
|
||||
int cmsg_type = 0;
|
||||
};
|
||||
|
||||
// Implements Android's exported CMSG_NXTHDR helper over the explicit Bionic
|
||||
// x86-64 msghdr/cmsghdr layout above. Malformed or truncated control buffers
|
||||
// fail closed with nullptr.
|
||||
BionicControlMessageHeader* BionicCmsgNextHeader(
|
||||
BionicMessageHeader* message, BionicControlMessageHeader* current) noexcept;
|
||||
|
||||
// Registers one destructor for the current thread using libstdc++'s Itanium
|
||||
// C++ ABI implementation. That implementation is present on both supported
|
||||
// glibc and musl toolchains and preserves destructor execution at thread exit.
|
||||
int BionicCxaThreadAtExit(BionicThreadDestructor destructor, void* argument,
|
||||
void* dso_handle) noexcept;
|
||||
|
||||
// Fills the complete buffer from the Linux kernel RNG. The implementation
|
||||
// falls back to /dev/urandom only when getrandom is unavailable or blocked and
|
||||
// aborts rather than returning predictable or partially initialized bytes.
|
||||
void BionicArc4RandomBuffer(void* buffer, size_t size) noexcept;
|
||||
|
||||
BionicMallinfoSnapshot BionicMallinfo() noexcept;
|
||||
bool BionicMallinfoHasHostTelemetry() noexcept;
|
||||
|
||||
// Formats one Linux errno value using Bionic's POSIX strerror_r contract.
|
||||
// Unlike glibc's GNU variant, success is reported as zero; truncation returns
|
||||
// -1 and sets errno to ERANGE while preserving the caller's errno on success.
|
||||
int BionicStrError(int error_number, char* buffer, size_t buffer_size) noexcept;
|
||||
|
||||
// Bionic supports only C and C.UTF-8, whose integer parsing is identical. A
|
||||
// host C locale is installed only for the duration of each call, so ambient
|
||||
// process locale cannot change guest parsing and a Bionic locale pointer is
|
||||
// never mistaken for an incompatible host locale_t.
|
||||
long long BionicStrToLongLongLocale(const char* text, char** end, int base,
|
||||
BionicLocale locale) noexcept;
|
||||
unsigned long long BionicStrToUnsignedLongLongLocale(
|
||||
const char* text, char** end, int base, BionicLocale locale) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
|
||||
mocktail::compat::BionicControlMessageHeader* mocktail_bionic_cmsg_nxthdr(
|
||||
mocktail::compat::BionicMessageHeader* message,
|
||||
mocktail::compat::BionicControlMessageHeader* current);
|
||||
int mocktail_bionic_cxa_thread_atexit_impl(
|
||||
mocktail::compat::BionicThreadDestructor destructor, void* argument,
|
||||
void* dso_handle);
|
||||
void mocktail_bionic_arc4random_buf(void* buffer, size_t size);
|
||||
mocktail::compat::BionicMallinfoSnapshot mocktail_bionic_mallinfo();
|
||||
int mocktail_bionic_strerror_r(int error_number, char* buffer,
|
||||
size_t buffer_size);
|
||||
long long mocktail_bionic_strtoll_l(const char* text, char** end, int base,
|
||||
mocktail::compat::BionicLocale locale);
|
||||
unsigned long long mocktail_bionic_strtoull_l(
|
||||
const char* text, char** end, int base,
|
||||
mocktail::compat::BionicLocale locale);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_HOST_LIBC_RUNTIME_H_
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_LARGE_FILE_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_LARGE_FILE_RUNTIME_H_
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android x86-64 defines off64_t as a signed 64-bit offset. A fixed-width
|
||||
// public type keeps the guest ABI independent of host feature-test macros.
|
||||
using BionicOff64 = int64_t;
|
||||
|
||||
ssize_t BionicPread64(int fd, void* buffer, size_t count,
|
||||
BionicOff64 offset) noexcept;
|
||||
ssize_t BionicPwrite64(int fd, const void* buffer, size_t count,
|
||||
BionicOff64 offset) noexcept;
|
||||
BionicOff64 BionicLseek64(int fd, BionicOff64 offset, int whence) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
|
||||
ssize_t mocktail_bionic_pread64(int fd, void* buffer, size_t count,
|
||||
mocktail::compat::BionicOff64 offset);
|
||||
ssize_t mocktail_bionic_pwrite64(int fd, const void* buffer, size_t count,
|
||||
mocktail::compat::BionicOff64 offset);
|
||||
mocktail::compat::BionicOff64
|
||||
mocktail_bionic_lseek64(int fd, mocktail::compat::BionicOff64 offset,
|
||||
int whence);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_LARGE_FILE_RUNTIME_H_
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
using NativeThreadInitializer = void (*)();
|
||||
|
||||
// Pinned Bionic LP64 pthread_internal.h keeps total per-thread reservation
|
||||
// near 1 MiB by subtracting its 32 KiB alternate signal stack from the normal
|
||||
// thread stack. Host defaults are not ABI substitutes: musl's default is only
|
||||
// 128 KiB on the supported Void runtime.
|
||||
inline constexpr size_t kBionicLp64DefaultThreadStackSize =
|
||||
(1U * 1024U * 1024U) - (32U * 1024U);
|
||||
static_assert(kBionicLp64DefaultThreadStackSize == 1015808U);
|
||||
|
||||
// Used only after a Bionic-sized stack is rejected by the host at thread
|
||||
// creation time. The retry takes the largest of this floor, the requested
|
||||
// guest size, and the host default. That retains enough stack on musl without
|
||||
// shrinking glibc's default.
|
||||
inline constexpr size_t kBionicLp64FallbackThreadStackSize = 2U * 1024U * 1024U;
|
||||
|
||||
// Installs the payload-owned per-thread initializer used by subsequently
|
||||
// created guest threads. Passing nullptr restores ordinary host behavior.
|
||||
void ConfigureBionicPthreadThreadInitializer(
|
||||
NativeThreadInitializer initializer) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
// Creates a host thread after rebuilding the supported POSIX attributes.
|
||||
// Rebuilding avoids passing ABI-private pthread_attr_t state from the Android
|
||||
// payload directly into glibc.
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument);
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_PTHREAD_KEY_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_PTHREAD_KEY_RUNTIME_H_
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android Bionic exposes pthread keys as 0x80000000 | index. Its current
|
||||
// public key pool includes two libc-reserved slots and PTHREAD_KEYS_MAX
|
||||
// application slots. The implementation mirrors that externally
|
||||
// visible ABI without replacing the host process TLS/FS layout.
|
||||
constexpr size_t kBionicPthreadKeyCount = 130;
|
||||
constexpr uint32_t kBionicPthreadKeyValidFlag = uint32_t{1} << 31;
|
||||
|
||||
using BionicPthreadKeyDestructor = void (*)(void*);
|
||||
|
||||
// implementation of Bionic's pthread_key_* ABI. It owns only guest
|
||||
// keys registered through the synthetic Android libc.so, while ordinary host
|
||||
// code continues to use glibc pthread keys.
|
||||
class BionicPthreadKeyRuntime {
|
||||
public:
|
||||
static BionicPthreadKeyRuntime& Instance() noexcept;
|
||||
|
||||
int Create(pthread_key_t* key, BionicPthreadKeyDestructor destructor) noexcept;
|
||||
int Delete(pthread_key_t key) noexcept;
|
||||
void* Get(pthread_key_t key) noexcept;
|
||||
int Set(pthread_key_t key, const void* value) noexcept;
|
||||
|
||||
private:
|
||||
BionicPthreadKeyRuntime() = default;
|
||||
};
|
||||
|
||||
bool IsBionicPthreadKey(pthread_key_t key) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_pthread_key_create(
|
||||
pthread_key_t* key, void (*destructor)(void*));
|
||||
int mocktail_bionic_pthread_key_delete(pthread_key_t key);
|
||||
void* mocktail_bionic_pthread_getspecific(pthread_key_t key);
|
||||
int mocktail_bionic_pthread_setspecific(pthread_key_t key,
|
||||
const void* value);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_PTHREAD_KEY_RUNTIME_H_
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_RWLOCK_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_RWLOCK_RUNTIME_H_
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
// Bionic and glibc pthread_rwlock_t storage is not ABI-compatible. The guest
|
||||
// address is an opaque key; the host object lives entirely in this runtime.
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_pthread_rwlock_init(pthread_rwlock_t* rwlock,
|
||||
const pthread_rwlockattr_t* attr);
|
||||
int mocktail_bionic_pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
|
||||
int mocktail_bionic_pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
|
||||
int mocktail_bionic_pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
|
||||
int mocktail_bionic_pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_RWLOCK_RUNTIME_H_
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_SEMAPHORE_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_SEMAPHORE_RUNTIME_H_
|
||||
|
||||
#include <semaphore.h>
|
||||
|
||||
// Bionic and glibc use different sem_t representations. These exports keep
|
||||
// host semaphore objects out of guest storage and key them by guest address.
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_sem_init(sem_t* semaphore, int process_shared,
|
||||
unsigned int value);
|
||||
int mocktail_bionic_sem_destroy(sem_t* semaphore);
|
||||
int mocktail_bionic_sem_wait(sem_t* semaphore);
|
||||
int mocktail_bionic_sem_post(sem_t* semaphore);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_SEMAPHORE_RUNTIME_H_
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_SIGNAL_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_SIGNAL_RUNTIME_H_
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android LP64 exposes a POSIX sigaction layout that differs
|
||||
// from both the Linux kernel and glibc layouts.
|
||||
struct BionicSigaction {
|
||||
int flags = 0;
|
||||
int padding = 0;
|
||||
union {
|
||||
void (*handler)(int);
|
||||
void (*action)(int, siginfo_t*, void*);
|
||||
} callback{};
|
||||
std::uint64_t mask = 0;
|
||||
void (*restorer)() = nullptr;
|
||||
};
|
||||
|
||||
static_assert(sizeof(BionicSigaction) == 32,
|
||||
"Android x86-64 sigaction ABI changed");
|
||||
|
||||
int BionicSigactionCall(int signal_number, const BionicSigaction* action,
|
||||
BionicSigaction* old_action) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" int mocktail_bionic_sigaction(
|
||||
int signal_number, const mocktail::compat::BionicSigaction* action,
|
||||
mocktail::compat::BionicSigaction* old_action);
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_SIGNAL_RUNTIME_H_
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_STDIO_RUNTIME_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_STDIO_RUNTIME_H_
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cwchar>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android's public LP64 FILE ABI is opaque but has a fixed 152-byte stride.
|
||||
// Old NDK clients derive stdin/stdout/stderr by indexing the exported __sF
|
||||
// array, so its size and alignment are part of the binary interface even when
|
||||
// all actual I/O is delegated to host stdio.
|
||||
constexpr size_t kBionicFileSize = 152;
|
||||
constexpr size_t kBionicStandardStreamCount = 3;
|
||||
|
||||
FILE* TranslateBionicFile(FILE* stream) noexcept;
|
||||
void* BionicFileArraySymbolAddress() noexcept;
|
||||
void* BionicStdinSymbolAddress() noexcept;
|
||||
void* BionicStdoutSymbolAddress() noexcept;
|
||||
void* BionicStderrSymbolAddress() noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
|
||||
alignas(sizeof(void*)) extern unsigned char
|
||||
__sF[mocktail::compat::kBionicStandardStreamCount]
|
||||
[mocktail::compat::kBionicFileSize];
|
||||
|
||||
}
|
||||
|
||||
// FILE values returned by host fopen/fdopen pass through unchanged. Pointers
|
||||
// into Bionic's ABI-sized __sF array are translated to the corresponding host
|
||||
// standard stream at this single boundary.
|
||||
extern "C" {
|
||||
|
||||
size_t mocktail_fwrite(const void* buffer, size_t size, size_t count,
|
||||
FILE* stream);
|
||||
size_t mocktail_fread(void* buffer, size_t size, size_t count, FILE* stream);
|
||||
int mocktail_fflush(FILE* stream);
|
||||
int mocktail_fclose(FILE* stream);
|
||||
int mocktail_feof(FILE* stream);
|
||||
int mocktail_ferror(FILE* stream);
|
||||
void mocktail_clearerr(FILE* stream);
|
||||
int mocktail_fileno(FILE* stream);
|
||||
int mocktail_fseek(FILE* stream, long offset, int whence);
|
||||
long mocktail_ftell(FILE* stream);
|
||||
int mocktail_fseeko(FILE* stream, off_t offset, int whence);
|
||||
off_t mocktail_ftello(FILE* stream);
|
||||
char* mocktail_fgets(char* string, int count, FILE* stream);
|
||||
int mocktail_fputc(int character, FILE* stream);
|
||||
int mocktail_fputs(const char* string, FILE* stream);
|
||||
int mocktail_getc(FILE* stream);
|
||||
wint_t mocktail_fputwc(wchar_t character, FILE* stream);
|
||||
int mocktail_setvbuf(FILE* stream, char* buffer, int mode, size_t size);
|
||||
int mocktail_ungetc(int character, FILE* stream);
|
||||
int mocktail_vfprintf(FILE* stream, const char* format, va_list arguments);
|
||||
int mocktail_fprintf(FILE* stream, const char* format, ...);
|
||||
int mocktail_vfscanf(FILE* stream, const char* format, va_list arguments);
|
||||
int mocktail_fscanf(FILE* stream, const char* format, ...);
|
||||
size_t mocktail___fread_chk(void* buffer, size_t buffer_size, size_t size,
|
||||
size_t count, FILE* stream);
|
||||
size_t mocktail___fwrite_chk(const void* buffer, size_t buffer_size,
|
||||
size_t size, size_t count, FILE* stream);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_STDIO_RUNTIME_H_
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BIONIC_SYSCONF_H_
|
||||
#define MOCKTAIL_COMPAT_BIONIC_SYSCONF_H_
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android Bionic assigns different integer values to sysconf names than
|
||||
// glibc. These are the names observed in the active Roblox Build-ID profile
|
||||
// and are kept explicit so a raw Bionic value can never reach host sysconf.
|
||||
enum class BionicSysconfName : int {
|
||||
kArgMax = 0x0000,
|
||||
kChildMax = 0x0005,
|
||||
kClockTicks = 0x0006,
|
||||
kOpenMax = 0x000b,
|
||||
kIovMax = 0x0026,
|
||||
kPageSize = 0x0027,
|
||||
kPageSizeAlias = 0x0028,
|
||||
kProcessorsConfigured = 0x0060,
|
||||
kProcessorsOnline = 0x0061,
|
||||
kPhysicalPages = 0x0062,
|
||||
kAvailablePhysicalPages = 0x0063,
|
||||
};
|
||||
|
||||
// Translates selected Bionic sysconf integer names to their host glibc
|
||||
// equivalent. Unsupported names fail exactly as Bionic does: -1 and EINVAL.
|
||||
long BionicSysconf(int name) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" long mocktail_bionic_sysconf(int name);
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_SYSCONF_H_
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
#define MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
enum class BuildStatus {
|
||||
kSupported,
|
||||
kExperimental,
|
||||
kLegacyResearched,
|
||||
kUnverified,
|
||||
};
|
||||
|
||||
struct FmodOutputDeviceBridgeProfile {
|
||||
std::uintptr_t vtable_rva = 0;
|
||||
std::uintptr_t string_constructor_rva = 0;
|
||||
std::uintptr_t count_method_rva = 0;
|
||||
std::uintptr_t info_method_rva = 0;
|
||||
std::uintptr_t current_method_rva = 0;
|
||||
std::uintptr_t select_method_rva = 0;
|
||||
};
|
||||
|
||||
struct BuildProfile {
|
||||
std::string version_name;
|
||||
int version_code = 0;
|
||||
std::string elf_build_id;
|
||||
BuildStatus status = BuildStatus::kUnverified;
|
||||
bool default_allowed = false;
|
||||
bool allow_legacy_binary_patches = false;
|
||||
bool allow_host_abi_bridges = false;
|
||||
bool allow_host_constructor_replay = false;
|
||||
// Optional, exact-build native capability. This is an invoked entrypoint,
|
||||
// never a writable binary patch; the adapter validates its code contract
|
||||
// before the first call.
|
||||
std::optional<std::uintptr_t> user_game_settings_fullscreen_setter_rva;
|
||||
// Optional exact-build host ABI bridge. It temporarily interposes only the
|
||||
// output-device virtual slots and restores them during controlled teardown;
|
||||
// executable guest code is never modified.
|
||||
std::optional<FmodOutputDeviceBridgeProfile> fmod_output_device_bridge;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct ProfileLookupResult {
|
||||
std::optional<BuildProfile> profile;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
ProfileLookupResult FindBuildProfile(const std::string& manifest_path,
|
||||
std::string_view build_id);
|
||||
|
||||
std::string_view BuildStatusName(BuildStatus status) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_ELF_BUILD_ID_H_
|
||||
#define MOCKTAIL_COMPAT_ELF_BUILD_ID_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
struct BuildIdResult {
|
||||
std::string build_id;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Reads the GNU build ID through elfutils libelf. The returned identifier is
|
||||
// lowercase hexadecimal and is empty only when an error is reported.
|
||||
BuildIdResult ReadElfBuildId(const std::string& path);
|
||||
|
||||
bool IsValidBuildId(std::string_view build_id) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_ELF_BUILD_ID_H_
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_HOST_ABI_EXPERIMENT_H_
|
||||
#define MOCKTAIL_COMPAT_HOST_ABI_EXPERIMENT_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "compat/host_abi_profile.h"
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
struct HostAbiBridgeTargets {
|
||||
void *allocate = nullptr;
|
||||
void *reallocate = nullptr;
|
||||
void *aligned_allocate = nullptr;
|
||||
void *free = nullptr;
|
||||
void *usable_size = nullptr;
|
||||
void *allocator_object_allocate = nullptr;
|
||||
void *null_vtable_stub = nullptr;
|
||||
};
|
||||
|
||||
struct HostAbiExperimentOptions {
|
||||
bool install_allocator_bridges = true;
|
||||
bool seed_allocator_object = true;
|
||||
bool seed_empty_string = true;
|
||||
bool initialize_arena = true;
|
||||
bool seed_jni_singleton = true;
|
||||
};
|
||||
|
||||
struct HostAbiExperimentResult {
|
||||
bool installed = false;
|
||||
bool uses_native_mimalloc = false;
|
||||
size_t bridge_entries_installed = 0;
|
||||
const char *error = nullptr;
|
||||
|
||||
explicit operator bool() const noexcept { return installed; }
|
||||
};
|
||||
|
||||
enum class NativeMimallocBootstrapStatus {
|
||||
kNotRequired,
|
||||
kInitialized,
|
||||
kFailed,
|
||||
};
|
||||
|
||||
enum class NativePreJniBootstrapStatus {
|
||||
kNotRequired,
|
||||
kAlreadyInitialized,
|
||||
kInitialized,
|
||||
kFailed,
|
||||
};
|
||||
|
||||
// Installs one exact-Build-ID experiment before libroblox constructors run.
|
||||
// The caller owns policy: this function never enables itself from environment
|
||||
// variables and must only receive a profile selected from the loaded ELF ID.
|
||||
HostAbiExperimentResult
|
||||
InstallHostAbiExperiment(uintptr_t library_base, const HostAbiProfile &profile,
|
||||
const HostAbiBridgeTargets &targets,
|
||||
const HostAbiExperimentOptions &options) noexcept;
|
||||
|
||||
// Calls the payload's idempotent native allocator TLS initializer on the
|
||||
// current thread. It must run on each thread that invokes native Roblox code.
|
||||
bool InitializeHostAbiThread(uintptr_t library_base,
|
||||
const HostAbiProfile &profile) noexcept;
|
||||
|
||||
// Applies Build-ID-scoped native mimalloc lifecycle ordering after one
|
||||
// constructor returns. No payload code is patched: this only invokes the
|
||||
// profile's existing native thread initializer at its verified boundary.
|
||||
NativeMimallocBootstrapStatus CompleteNativeMimallocConstructor(
|
||||
uintptr_t library_base, const HostAbiProfile &profile,
|
||||
size_t constructor_index) noexcept;
|
||||
|
||||
// Runs one verified native registry initializer after selected static
|
||||
// constructors and before JNI_OnLoad. The payload owns both allocation and
|
||||
// storage; Mocktail only checks the profile-defined postcondition.
|
||||
NativePreJniBootstrapStatus InitializeNativePreJniRegistry(
|
||||
uintptr_t library_base, const HostAbiProfile &profile) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HOST_ABI_EXPERIMENT_H_
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_HOST_ABI_PROFILE_H_
|
||||
#define MOCKTAIL_COMPAT_HOST_ABI_PROFILE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
enum class HostBridgeKind {
|
||||
kAllocate,
|
||||
kReallocate,
|
||||
kAlignedAllocate,
|
||||
kFree,
|
||||
kUsableSize,
|
||||
};
|
||||
|
||||
enum class HostAllocatorStrategy {
|
||||
kHostBridges,
|
||||
kNativeMimalloc,
|
||||
};
|
||||
|
||||
struct HostBridgeEntry {
|
||||
uintptr_t rva = 0;
|
||||
HostBridgeKind kind = HostBridgeKind::kAllocate;
|
||||
std::string_view label;
|
||||
};
|
||||
|
||||
struct ConstructorRange {
|
||||
size_t begin = 0;
|
||||
size_t end_exclusive = 0;
|
||||
|
||||
bool IsValidFor(size_t constructor_count) const noexcept {
|
||||
return begin < end_exclusive && end_exclusive <= constructor_count;
|
||||
}
|
||||
};
|
||||
|
||||
struct HostDataSeedProfile {
|
||||
uintptr_t allocator_object_slot = 0;
|
||||
uintptr_t empty_string_slot = 0;
|
||||
uintptr_t jni_singleton_slot = 0;
|
||||
size_t jni_singleton_bytes = 0;
|
||||
uintptr_t arena_initializer = 0;
|
||||
uintptr_t allocator_thread_initializer = 0;
|
||||
uintptr_t arena_guard_slot = 0;
|
||||
uintptr_t arena_table_slot = 0;
|
||||
size_t arena_table_slot_count = 0;
|
||||
};
|
||||
|
||||
struct NativePreJniBootstrapProfile {
|
||||
uintptr_t registry_initializer = 0;
|
||||
uintptr_t registry_slot = 0;
|
||||
|
||||
bool IsValid() const noexcept {
|
||||
return registry_initializer != 0 && registry_slot != 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct NativeAllocatorProfile {
|
||||
uintptr_t allocate = 0;
|
||||
uintptr_t deallocate = 0;
|
||||
|
||||
bool IsValid() const noexcept { return allocate != 0 && deallocate != 0; }
|
||||
};
|
||||
|
||||
inline constexpr size_t kMaxHostBridgeEntries = 8;
|
||||
inline constexpr size_t kMaxConstructorRunRanges = 26;
|
||||
|
||||
// Build-ID-scoped native ABI knowledge for supported and experimental payload
|
||||
// profiles. Keeping these addresses outside the legacy startup graph makes the
|
||||
// version boundary reviewable and independently testable.
|
||||
struct HostAbiProfile {
|
||||
std::string_view elf_build_id;
|
||||
std::array<HostBridgeEntry, kMaxHostBridgeEntries> bridge_entries{};
|
||||
size_t bridge_entry_count = 0;
|
||||
HostDataSeedProfile data_seeds;
|
||||
NativeAllocatorProfile native_allocator;
|
||||
uintptr_t init_array_offset = 0;
|
||||
size_t init_array_count = 0;
|
||||
std::array<ConstructorRange, kMaxConstructorRunRanges>
|
||||
constructor_run_ranges{};
|
||||
size_t constructor_run_range_count = 0;
|
||||
// Verified native-mimalloc-compatible replay that keeps the payload's
|
||||
// dependency-ordered pre-JNI static state coherent. This is distinct from
|
||||
// the host bridge bring-up path and must not use host malloc trampolines.
|
||||
std::array<ConstructorRange, kMaxConstructorRunRanges>
|
||||
native_mimalloc_constructor_run_ranges{};
|
||||
size_t native_mimalloc_constructor_run_range_count = 0;
|
||||
// The native process-load constructor has completed at this index. The
|
||||
// per-thread initializer must run before the following integration probe.
|
||||
size_t native_mimalloc_thread_initializer_after_constructor = 0;
|
||||
// Exact payload initializer that constructs the native registry needed by
|
||||
// JNI callbacks registered during the selected constructor replay.
|
||||
NativePreJniBootstrapProfile native_pre_jni_bootstrap;
|
||||
// Strategy used when MOCKTAIL_HOST_ALLOCATOR_BRIDGES has no explicit
|
||||
// override. New verified payloads retain their native allocator; researched
|
||||
// legacy profiles keep the historical host-bridge default.
|
||||
HostAllocatorStrategy default_allocator_strategy =
|
||||
HostAllocatorStrategy::kHostBridges;
|
||||
|
||||
bool HasValidConstructorRanges() const noexcept;
|
||||
bool AllowsConstructor(size_t index) const noexcept;
|
||||
size_t ConstructorRangeBegin() const noexcept;
|
||||
size_t ConstructorRangeEndExclusive() const noexcept;
|
||||
bool HasValidNativeMimallocConstructorRanges() const noexcept;
|
||||
bool AllowsNativeMimallocConstructor(size_t index) const noexcept;
|
||||
size_t NativeMimallocConstructorRangeBegin() const noexcept;
|
||||
size_t NativeMimallocConstructorRangeEndExclusive() const noexcept;
|
||||
bool ShouldInitializeNativeMimallocThreadAfterConstructor(
|
||||
size_t index) const noexcept;
|
||||
bool HasValidNativePreJniBootstrap() const noexcept;
|
||||
HostAllocatorStrategy ResolveAllocatorStrategy(
|
||||
bool host_bridges_override_present,
|
||||
bool host_bridges_override_enabled) const noexcept;
|
||||
};
|
||||
|
||||
const HostAbiProfile *FindHostAbiProfile(std::string_view build_id) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HOST_ABI_PROFILE_H_
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_
|
||||
#define MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "compat/host_abi_profile.h"
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
struct FileSha256Result {
|
||||
std::string sha256;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Computes a lowercase SHA-256 digest without invoking a shell or an external
|
||||
// utility. The external ABI approval receipt binds exact profile,
|
||||
// compatibility-manifest, and payload bytes through this implementation; the
|
||||
// runtime itself is bound by its stable GNU Build ID because installation may
|
||||
// rewrite RPATH bytes.
|
||||
FileSha256Result ComputeFileSha256(const std::string& path);
|
||||
|
||||
struct ExternalHostAbiProfileRequest {
|
||||
std::string profile_file;
|
||||
std::string payload_path;
|
||||
std::string compatibility_manifest_path;
|
||||
std::string expected_build_id;
|
||||
std::string approval_receipt_path;
|
||||
bool candidate_canary = false;
|
||||
bool candidate_process_authorization = false;
|
||||
bool explicit_unverified_authorization = false;
|
||||
};
|
||||
|
||||
struct ExternalHostAbiProfileResult {
|
||||
const HostAbiProfile* profile = nullptr;
|
||||
std::string payload_sha256;
|
||||
std::string profile_sha256;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Loads one exact, payload-bound ABI profile into the process registry. A
|
||||
// candidate is accepted only by an explicitly authorized canary process. A
|
||||
// normal launch instead requires a separate approval receipt produced after
|
||||
// repeated Tier C readiness. Built-in profiles are immutable and always win.
|
||||
ExternalHostAbiProfileResult LoadExternalHostAbiProfile(
|
||||
const ExternalHostAbiProfileRequest& request);
|
||||
|
||||
// Environment integration used by the pre-native compatibility gate. An
|
||||
// unset MOCKTAIL_HOST_ABI_PROFILE_FILE is a successful no-op. If it is set,
|
||||
// candidate authorization requires MOCKTAIL_HOST_ABI_CANARY=1,
|
||||
// MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1, and the explicit research command-line
|
||||
// marker. Approved profiles require MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT and
|
||||
// neither canary environment marker.
|
||||
ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment(
|
||||
const std::string& payload_path,
|
||||
const std::string& compatibility_manifest_path,
|
||||
const std::string& expected_build_id,
|
||||
bool explicit_unverified_authorization);
|
||||
|
||||
// Registry lookup used by FindHostAbiProfile after its immutable built-in
|
||||
// table. Callers should use FindHostAbiProfile rather than this function.
|
||||
const HostAbiProfile* FindLoadedExternalHostAbiProfile(
|
||||
std::string_view build_id) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_HOST_ALLOCATOR_BRIDGE_H_
|
||||
#define MOCKTAIL_COMPAT_HOST_ALLOCATOR_BRIDGE_H_
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Explicit smoke-only ownership boundary for allocations returned to the
|
||||
// guest. Unknown/native pointers are never inspected or freed.
|
||||
void* HostAllocate(size_t size) noexcept;
|
||||
void* HostAlignedAllocate(size_t size, size_t alignment) noexcept;
|
||||
void* HostReallocate(void* pointer, size_t size) noexcept;
|
||||
void HostFree(void* pointer) noexcept;
|
||||
size_t HostUsableSize(void* pointer) noexcept;
|
||||
void* HostAllocatorObjectAllocate(void* object, size_t size,
|
||||
size_t alignment) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HOST_ALLOCATOR_BRIDGE_H_
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_HTTP_CLIENT_SPIN_GUARD_H_
|
||||
#define MOCKTAIL_COMPAT_HTTP_CLIENT_SPIN_GUARD_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
namespace compat {
|
||||
|
||||
constexpr std::uint32_t kHttpClientSpinBatchSize = 256;
|
||||
constexpr std::uint64_t kHttpClientSpinBatchIntervalNs = 1000000;
|
||||
|
||||
// Pure rate-limiter state used by the process-local HttpClient guard. A normal
|
||||
// request that does not complete a full mutex batch is never delayed.
|
||||
class HttpClientSpinRateLimiter {
|
||||
public:
|
||||
std::uint64_t Observe(std::uint64_t now_ns);
|
||||
|
||||
private:
|
||||
std::uint32_t operation_count_ = 0;
|
||||
std::uint64_t checkpoint_ns_ = 0;
|
||||
};
|
||||
|
||||
// Called after a successful Bionic mutex unlock. Only a thread named exactly
|
||||
// "HttpClient" is rate-limited, and only when it completes mutex batches
|
||||
// faster than the cooperative one-millisecond interval.
|
||||
void ApplyHttpClientSpinGuard();
|
||||
|
||||
} // namespace compat
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HTTP_CLIENT_SPIN_GUARD_H_
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_COMPAT_PAYLOAD_COMPATIBILITY_H_
|
||||
#define MOCKTAIL_COMPAT_PAYLOAD_COMPATIBILITY_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace compat {
|
||||
|
||||
// Result of the side-effect-free compatibility gate used before any network,
|
||||
// authentication, or native-loader work. The legacy runtime repeats the
|
||||
// profile application while that ownership is migrated, but external startup
|
||||
// services must not run until this gate succeeds.
|
||||
struct PayloadCompatibilityResult {
|
||||
BuildProfile profile;
|
||||
std::string build_id;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
PayloadCompatibilityResult CheckPayloadCompatibility(
|
||||
const std::string& library_path, const std::string& manifest_path,
|
||||
bool allow_unverified_build);
|
||||
|
||||
} // namespace compat
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_PAYLOAD_COMPATIBILITY_H_
|
||||
@@ -0,0 +1,508 @@
|
||||
// Copyright 2026 Sober Test Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// jnivm/jnivm.h — Pseudo Java Native Interface Virtual Machine.
|
||||
//
|
||||
// Provides a lightweight C++ implementation of a JVM stub for use with
|
||||
// Android-native shared libraries (e.g., libroblox.so) running on Linux.
|
||||
// No real Java bytecode is executed; instead, all JNI calls are intercepted
|
||||
// and dispatched to registered C++ callbacks.
|
||||
//
|
||||
// Usage:
|
||||
// auto vm = std::make_shared<jnivm::VM>();
|
||||
// auto cls = vm->RegisterClass("android/content/Context");
|
||||
// cls->RegisterMethod("getSystemService",
|
||||
// "(Ljava/lang/String;)Ljava/lang/Object;",
|
||||
// [](JNIEnv*, jobject, ...) { return nullptr; });
|
||||
|
||||
#ifndef SOBER_TEST_JNIVM_JNIVM_H_
|
||||
#define SOBER_TEST_JNIVM_JNIVM_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
// Forward declarations
|
||||
|
||||
class VM;
|
||||
class Class;
|
||||
|
||||
// Object — base type for all pseudo-Java objects managed by the pseudo-JVM.
|
||||
// Analogous to java.lang.Object in the real JVM.
|
||||
|
||||
class Object {
|
||||
public:
|
||||
explicit Object(std::shared_ptr<Class> klass) : klass_(std::move(klass)) {}
|
||||
virtual ~Object() = default;
|
||||
|
||||
// Returns the pseudo-class descriptor of this object.
|
||||
std::shared_ptr<Class> GetClass() const { return klass_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Class> klass_;
|
||||
};
|
||||
|
||||
// MethodCallback — signature for registered JNI method stubs.
|
||||
// The callback receives the raw JNIEnv pointer and the receiver jobject.
|
||||
|
||||
using MethodCallback = std::function<void(JNIEnv *, jobject)>;
|
||||
|
||||
// Resolved Roblox account identity supplied by the host runtime. This
|
||||
// intentionally contains no cookie or other authentication credential.
|
||||
struct RobloxAuthIdentity {
|
||||
int64_t user_id = -1;
|
||||
std::string username;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
// Host device capabilities exposed through the pseudo Android framework.
|
||||
// The guest ABI remains Android, while the default profile // describes a Linux desktop with physical mouse and keyboard input.
|
||||
struct PlatformIdentity {
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
bool keyboard_enabled = true;
|
||||
bool pc_hardware = true;
|
||||
std::string platform_name = "Linux";
|
||||
std::string device_name = "Mocktail Headless";
|
||||
std::string manufacturer = "Mocktail";
|
||||
std::string model = "Mocktail Headless";
|
||||
std::string brand = "Mocktail";
|
||||
std::string device_code = "linux-x86_64";
|
||||
std::string device_sku = "mocktail-x86_64";
|
||||
std::string soc_model = "x86_64";
|
||||
};
|
||||
|
||||
struct RobloxCredentialView {
|
||||
const char *data = nullptr;
|
||||
std::size_t size = 0;
|
||||
};
|
||||
|
||||
using RobloxCredentialProvider = RobloxCredentialView (*)(const void *context);
|
||||
|
||||
// Creates a Configuration object consistent with the VM's immutable platform
|
||||
// identity snapshot. This is the replacement for legacy construction of
|
||||
// contradictory Android keyboard/touch fields.
|
||||
jobject CreateAndroidConfiguration(JNIEnv *env);
|
||||
|
||||
// Narrow write-only boundary for credentials produced by Roblox after the
|
||||
// native runtime has started. A successfully persisted value supersedes the
|
||||
// bound provider for the remainder of the current VM lifetime so native sign-in
|
||||
// can become authoritative without reopening an untrusted credential source.
|
||||
struct RobloxCredentialSinkCallbacks {
|
||||
bool (*store)(void *context, const char *data, std::size_t size) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for org/fmod/AudioDevice. The pseudo-JVM owns
|
||||
// only an opaque shared context and never depends on the SDL audio runtime.
|
||||
// Each Java receiver is passed as a stable identity so the host can keep
|
||||
// independent device lifecycles without retaining a JNI local reference.
|
||||
struct FmodAudioDeviceCallbacks {
|
||||
bool (*init)(void *context, const void *identity, int channels,
|
||||
int sample_rate_hz, int block_size_frames,
|
||||
int block_count) = nullptr;
|
||||
bool (*write)(void *context, const void *identity, const std::uint8_t *data,
|
||||
std::size_t size) = nullptr;
|
||||
bool (*close)(void *context, const void *identity) = nullptr;
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for the activity.setWindowFlags(II)V contract.
|
||||
// Production LuaApp retains its activity through the android.app.Activity
|
||||
// interface, while GameActivity startup uses the concrete subclass. Guest JNI
|
||||
// threads only publish Android flags; the platform callback must marshal any
|
||||
// SDL work onto the SDL main thread.
|
||||
struct AndroidWindowCallbacks {
|
||||
bool (*set_flags)(void *context, int flags, int mask) = nullptr;
|
||||
};
|
||||
|
||||
// Immutable snapshot of the Android NativeTextBoxInfo payload supplied by
|
||||
// Roblox when a native text field receives focus.
|
||||
struct RobloxTextBoxInfo {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
float font_size = 0.0f;
|
||||
bool multiline = false;
|
||||
int x_alignment = 0;
|
||||
int y_alignment = 0;
|
||||
int text_color = 0;
|
||||
int font = 0;
|
||||
int text_input_type = 0;
|
||||
int return_key_type = 0;
|
||||
bool manual_focus_release = false;
|
||||
bool text_wrapped = false;
|
||||
};
|
||||
|
||||
struct RobloxTextInputShowRequest {
|
||||
std::int64_t text_box = 0;
|
||||
bool show_native_input = false;
|
||||
std::string text;
|
||||
RobloxTextBoxInfo info;
|
||||
};
|
||||
|
||||
// Narrow callback boundary for the Roblox native text-input callbacks. The
|
||||
// pseudo-JVM snapshots all JNI-owned arguments before entering this boundary.
|
||||
struct RobloxTextInputCallbacks {
|
||||
void (*show)(void *context,
|
||||
const RobloxTextInputShowRequest &request) = nullptr;
|
||||
void (*hide)(void *context) = nullptr;
|
||||
void (*replace_text)(void *context, const std::string &text) = nullptr;
|
||||
void (*properties_changed)(void *context) = nullptr;
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for a native MessageBus RawCallback object. The VM
|
||||
// retains context while a callback is in flight and dispatches only the exact
|
||||
// run(Ljava/lang/String;)V method on the exact created receiver.
|
||||
struct MessageBusRawCallbacks {
|
||||
void (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
|
||||
struct MessageBusRequestHandlerCallbacks {
|
||||
std::string (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the current APK's legacy BrowserService transport.
|
||||
// Dispatch accepts only Callback.onItemSet(Ljava/lang/String;)V on the exact
|
||||
// pseudo receiver created by CreateMemStorageCallback.
|
||||
struct MemStorageCallbackCallbacks {
|
||||
void (*on_item_set)(void *context, JNIEnv *env, jstring value) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact APK callbacks that update host web
|
||||
// state: NativeGLJavaInterface notifications, web activities, cookie sync,
|
||||
// and JNICookieProtocol.OnSetCookieHandler. The VM retains the shared callback
|
||||
// context while a callback is in flight; consumers must copy JNI strings
|
||||
// before returning.
|
||||
struct RobloxDataModelNotificationCallbacks {
|
||||
void (*on_notification)(void *context, JNIEnv *env, jstring type,
|
||||
jstring data) = nullptr;
|
||||
void (*on_app_bridge_notification)(void* context, JNIEnv* env, jstring type,
|
||||
jstring data) = nullptr;
|
||||
void (*on_native_overlay)(void* context, JNIEnv* env, jstring title,
|
||||
jstring url) = nullptr;
|
||||
void (*on_open_web_activity)(void* context, JNIEnv* env, jstring url,
|
||||
jstring title) = nullptr;
|
||||
void (*on_sync_cookies)(void* context, JNIEnv* env, jstring cookie) = nullptr;
|
||||
void (*on_set_cookie)(void* context, JNIEnv* env, jstring cookie,
|
||||
jstring url) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact NativeHelper lifecycle notification
|
||||
// emitted when Roblox returns from an experience to LuaApp.
|
||||
struct RobloxExperienceLifecycleCallbacks {
|
||||
void (*on_lua_app_did_return)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Class — represents a pseudo Java class registered in the pseudo-JVM.
|
||||
// Stores the class name (JNI descriptor format) and a map of method stubs.
|
||||
|
||||
class Class {
|
||||
public:
|
||||
explicit Class(std::string name) : name_(std::move(name)) {}
|
||||
|
||||
// Returns the JNI-style class descriptor, e.g. "android/content/Context".
|
||||
const std::string &GetName() const { return name_; }
|
||||
|
||||
// Registers a JNI method stub.
|
||||
//
|
||||
// Args:
|
||||
// method_name: Simple method name (e.g. "getSystemService").
|
||||
// signature: JNI type descriptor (e.g. "(Ljava/lang/String;)V").
|
||||
// callback: C++ lambda invoked when the method is called via JNI.
|
||||
void RegisterMethod(const std::string &method_name,
|
||||
const std::string &signature, MethodCallback callback);
|
||||
|
||||
// Looks up a method stub by name and signature.
|
||||
// Returns nullptr if not found.
|
||||
const MethodCallback *FindMethod(const std::string &method_name,
|
||||
const std::string &signature) const;
|
||||
|
||||
// Returns all registered methods (name → callback).
|
||||
const std::unordered_map<std::string, MethodCallback> &GetMethods() const {
|
||||
return methods_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
|
||||
// Key format: "methodName:descriptor"
|
||||
std::unordered_map<std::string, MethodCallback> methods_;
|
||||
};
|
||||
|
||||
// VM — the top-level pseudo Java Virtual Machine.
|
||||
//
|
||||
// Owns a registry of pseudo-classes and exposes a JavaVM* compatible
|
||||
// pointer for passing to JNI_OnLoad in the loaded shared library.
|
||||
|
||||
class VM {
|
||||
public:
|
||||
VM();
|
||||
~VM();
|
||||
|
||||
// Disallow copy and assign.
|
||||
VM(const VM &) = delete;
|
||||
VM &operator=(const VM &) = delete;
|
||||
|
||||
// Registers a new pseudo-class by its JNI descriptor.
|
||||
// Repeated registration returns the existing instance.
|
||||
//
|
||||
// Args:
|
||||
// class_name: JNI-style descriptor, e.g.
|
||||
// "com/roblox/client/RobloxActivity".
|
||||
//
|
||||
// Returns:
|
||||
// Shared pointer to the registered (or pre-existing) Class object.
|
||||
std::shared_ptr<Class> RegisterClass(const std::string &class_name);
|
||||
|
||||
// Finds a previously registered class.
|
||||
// Returns nullptr if the class has not been registered.
|
||||
std::shared_ptr<Class> FindClass(const std::string &class_name) const;
|
||||
|
||||
// Returns the raw JavaVM pointer compatible with JNI_OnLoad signature.
|
||||
JavaVM *GetJavaVM() { return java_vm_; }
|
||||
|
||||
// Resolves the process pseudo-VM that owns an exact JavaVM pointer. Returns
|
||||
// null for foreign VMs and after the owner is destroyed.
|
||||
static VM *FromJavaVM(JavaVM *java_vm);
|
||||
|
||||
// Returns the attached JNIEnv pointer for the current host thread.
|
||||
JNIEnv *GetJNIEnv();
|
||||
|
||||
// Returns the number of registered classes.
|
||||
std::size_t GetClassCount() const { return class_registry_.size(); }
|
||||
|
||||
// Replaces the resolved account identity with an independent copy. Invalid
|
||||
// (non-positive) identities are normalized to the unresolved state.
|
||||
void SetRobloxAuthIdentity(const RobloxAuthIdentity &identity);
|
||||
|
||||
// Returns the VM to its unresolved account state.
|
||||
void ClearRobloxAuthIdentity();
|
||||
|
||||
// Returns an immutable-by-value snapshot safe for use by JNI callbacks on
|
||||
// another thread.
|
||||
RobloxAuthIdentity GetRobloxAuthIdentitySnapshot() const;
|
||||
|
||||
void SetPlatformIdentity(const PlatformIdentity &identity);
|
||||
PlatformIdentity GetPlatformIdentitySnapshot() const;
|
||||
|
||||
// Installs a non-owning production credential provider. A configured
|
||||
// provider returning an empty view blocks legacy env/disk
|
||||
// fallback for guest sessions. The provider owner must outlive all JNI
|
||||
// calls and clear the provider only after native workers have stopped.
|
||||
void SetRobloxCredentialProvider(const void *context,
|
||||
RobloxCredentialProvider provider);
|
||||
void ClearRobloxCredentialProvider();
|
||||
|
||||
// Returns true when the authoritative provider is configured, including an
|
||||
// explicitly empty guest credential. A credential accepted by the sink is
|
||||
// retained privately and returned instead until the provider is cleared.
|
||||
bool CopyRobloxCredentialFromProvider(std::string *credential) const;
|
||||
|
||||
void SetRobloxCredentialSink(std::shared_ptr<void> context,
|
||||
const RobloxCredentialSinkCallbacks &callbacks);
|
||||
void ClearRobloxCredentialSink();
|
||||
bool DispatchRobloxCredential(const char *data, std::size_t size);
|
||||
|
||||
// Installs an org/fmod/AudioDevice backend. The VM retains context for
|
||||
// every in-flight callback and calls shutdown when the binding is cleared or
|
||||
// the VM is destroyed. Replacing or clearing a binding is only safe after
|
||||
// guest JNI worker threads have stopped.
|
||||
void SetFmodAudioDeviceCallbacks(std::shared_ptr<void> context,
|
||||
const FmodAudioDeviceCallbacks &callbacks);
|
||||
void ClearFmodAudioDeviceCallbacks();
|
||||
|
||||
// Internal dispatch entry points used by the JNI function table. They are
|
||||
// public because JNI requires captureless C-compatible callbacks.
|
||||
bool DispatchFmodAudioDeviceInit(const void *identity, int channels,
|
||||
int sample_rate_hz, int block_size_frames,
|
||||
int block_count);
|
||||
bool DispatchFmodAudioDeviceWrite(const void *identity,
|
||||
const std::uint8_t *data, std::size_t size);
|
||||
bool DispatchFmodAudioDeviceClose(const void *identity);
|
||||
|
||||
void SetAndroidWindowCallbacks(std::shared_ptr<void> context,
|
||||
const AndroidWindowCallbacks &callbacks);
|
||||
void ClearAndroidWindowCallbacks();
|
||||
bool DispatchAndroidWindowFlags(int flags, int mask);
|
||||
|
||||
void SetRobloxTextInputCallbacks(std::shared_ptr<void> context,
|
||||
const RobloxTextInputCallbacks &callbacks);
|
||||
void ClearRobloxTextInputCallbacks();
|
||||
bool DispatchRobloxTextInputShow(const RobloxTextInputShowRequest &request);
|
||||
bool DispatchRobloxTextInputHide();
|
||||
bool DispatchRobloxTextInputReplaceText(const std::string &text);
|
||||
bool DispatchRobloxTextInputPropertiesChanged();
|
||||
|
||||
// Creates a pseudo com.roblox.universalapp.messagebus.RawCallback receiver
|
||||
// backed by a host callback. Clear removes future dispatch while an
|
||||
// already-running call keeps its shared context alive until return.
|
||||
jobject CreateMessageBusRawCallback(std::shared_ptr<void> context,
|
||||
const MessageBusRawCallbacks &callbacks);
|
||||
void ClearMessageBusRawCallback(jobject callback);
|
||||
bool DispatchMessageBusRawCallback(jobject callback, JNIEnv *env,
|
||||
jstring message);
|
||||
jobject CreateMessageBusRequestHandler(
|
||||
std::shared_ptr<void> context,
|
||||
const MessageBusRequestHandlerCallbacks &callbacks);
|
||||
void ClearMessageBusRequestHandler(jobject handler);
|
||||
jstring DispatchMessageBusRequestHandler(jobject handler, JNIEnv *env,
|
||||
jstring message);
|
||||
|
||||
// Creates the exact com.roblox.engine.jni.memstorage.Callback receiver used
|
||||
// by MemStorage.bind. Clear prevents future dispatch while an in-flight call
|
||||
// retains its shared context until the callback returns.
|
||||
jobject CreateMemStorageCallback(
|
||||
std::shared_ptr<void> context,
|
||||
const MemStorageCallbackCallbacks& callbacks);
|
||||
void ClearMemStorageCallback(jobject callback);
|
||||
bool DispatchMemStorageCallback(jobject callback, JNIEnv *env, jstring value);
|
||||
|
||||
// Installs the platform web callbacks reached through NativeGLJavaInterface,
|
||||
// MainGameActivity, and JNICookieProtocol. Clear prevents future dispatch
|
||||
// while an in-flight call retains its shared context until return.
|
||||
void SetRobloxDataModelNotificationCallbacks(
|
||||
std::shared_ptr<void> context,
|
||||
const RobloxDataModelNotificationCallbacks &callbacks);
|
||||
void ClearRobloxDataModelNotificationCallbacks();
|
||||
bool DispatchRobloxDataModelNotification(JNIEnv *env, jstring type,
|
||||
jstring data);
|
||||
bool DispatchRobloxAppBridgeNotification(JNIEnv* env, jstring type,
|
||||
jstring data);
|
||||
bool DispatchRobloxNativeOverlay(JNIEnv* env, jstring title, jstring url);
|
||||
bool DispatchRobloxOpenWebActivity(JNIEnv* env, jstring url, jstring title);
|
||||
bool DispatchRobloxCookieSync(JNIEnv* env, jstring cookie);
|
||||
bool DispatchRobloxCookieSet(JNIEnv* env, jobjectArray cookies, jstring url);
|
||||
|
||||
// Installs the callback used by the exact NativeHelper
|
||||
// gameActivity_onLuaAppDidReturn()V and ASMA/V2 NativeGLJavaInterface
|
||||
// gameDidLeave()V contracts. Dispatch snapshots the shared binding so Clear
|
||||
// cannot destroy context while a call is in flight.
|
||||
void SetRobloxExperienceLifecycleCallbacks(
|
||||
std::shared_ptr<void> context,
|
||||
const RobloxExperienceLifecycleCallbacks &callbacks);
|
||||
void ClearRobloxExperienceLifecycleCallbacks();
|
||||
bool DispatchRobloxExperienceLuaAppDidReturn();
|
||||
|
||||
// Restores env->functions to &native_interface_ in case libroblox replaced
|
||||
// it.
|
||||
void RestoreFunctions();
|
||||
|
||||
private:
|
||||
// Internal JNI function tables populated with stub implementations.
|
||||
JNIInvokeInterface_ invoke_interface_ = {};
|
||||
JavaVM java_vm_storage_ = {};
|
||||
JavaVM *java_vm_ = nullptr;
|
||||
|
||||
JNINativeInterface_ native_interface_ = {};
|
||||
JNIEnv jni_env_storage_ = {};
|
||||
JNIEnv *jni_env_ = nullptr;
|
||||
|
||||
// Registry of pseudo-classes indexed by their JNI descriptor.
|
||||
std::unordered_map<std::string, std::shared_ptr<Class>> class_registry_;
|
||||
|
||||
mutable std::mutex roblox_auth_identity_mutex_;
|
||||
RobloxAuthIdentity roblox_auth_identity_;
|
||||
|
||||
mutable std::mutex platform_identity_mutex_;
|
||||
PlatformIdentity platform_identity_;
|
||||
|
||||
mutable std::mutex roblox_credential_provider_mutex_;
|
||||
const void *roblox_credential_provider_context_ = nullptr;
|
||||
RobloxCredentialProvider roblox_credential_provider_ = nullptr;
|
||||
std::string roblox_credential_override_;
|
||||
|
||||
struct RobloxCredentialSinkBinding {
|
||||
std::shared_ptr<void> context;
|
||||
RobloxCredentialSinkCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex roblox_credential_sink_mutex_;
|
||||
RobloxCredentialSinkBinding roblox_credential_sink_binding_;
|
||||
|
||||
struct FmodAudioDeviceBinding {
|
||||
std::shared_ptr<void> context;
|
||||
FmodAudioDeviceCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex fmod_audio_device_mutex_;
|
||||
FmodAudioDeviceBinding fmod_audio_device_binding_;
|
||||
|
||||
struct AndroidWindowBinding {
|
||||
std::shared_ptr<void> context;
|
||||
AndroidWindowCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex android_window_mutex_;
|
||||
AndroidWindowBinding android_window_binding_;
|
||||
|
||||
struct RobloxTextInputBinding;
|
||||
mutable std::mutex roblox_text_input_mutex_;
|
||||
std::shared_ptr<RobloxTextInputBinding> roblox_text_input_binding_;
|
||||
|
||||
struct MessageBusRawBinding {
|
||||
std::shared_ptr<void> context;
|
||||
MessageBusRawCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex message_bus_raw_mutex_;
|
||||
std::unordered_map<jobject, std::shared_ptr<MessageBusRawBinding>>
|
||||
message_bus_raw_bindings_;
|
||||
|
||||
struct MessageBusRequestHandlerBinding {
|
||||
std::shared_ptr<void> context;
|
||||
MessageBusRequestHandlerCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex message_bus_request_handler_mutex_;
|
||||
std::unordered_map<jobject, std::shared_ptr<MessageBusRequestHandlerBinding>>
|
||||
message_bus_request_handler_bindings_;
|
||||
|
||||
struct MemStorageCallbackBinding {
|
||||
std::shared_ptr<void> context;
|
||||
MemStorageCallbackCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex mem_storage_callback_mutex_;
|
||||
std::unordered_map<jobject, std::shared_ptr<MemStorageCallbackBinding>>
|
||||
mem_storage_callback_bindings_;
|
||||
|
||||
struct RobloxDataModelNotificationBinding {
|
||||
std::shared_ptr<void> context;
|
||||
RobloxDataModelNotificationCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex roblox_data_model_notification_mutex_;
|
||||
std::shared_ptr<RobloxDataModelNotificationBinding>
|
||||
roblox_data_model_notification_binding_;
|
||||
|
||||
struct RobloxExperienceLifecycleBinding {
|
||||
std::shared_ptr<void> context;
|
||||
RobloxExperienceLifecycleCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex roblox_experience_lifecycle_mutex_;
|
||||
std::shared_ptr<RobloxExperienceLifecycleBinding>
|
||||
roblox_experience_lifecycle_binding_;
|
||||
|
||||
// Initialises the JNIInvokeInterface_ and JNINativeInterface_ function tables
|
||||
// with stub implementations that forward calls to this VM instance.
|
||||
void InitJNIFunctionTables();
|
||||
};
|
||||
|
||||
} // namespace jnivm
|
||||
|
||||
#endif // SOBER_TEST_JNIVM_JNIVM_H_
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_LEGACY_LEGACY_RUNTIME_H_
|
||||
#define MOCKTAIL_LEGACY_LEGACY_RUNTIME_H_
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/auth_runtime_composition.h"
|
||||
#include "runtime/command_line.h"
|
||||
|
||||
namespace jnivm {
|
||||
class VM;
|
||||
} // namespace jnivm
|
||||
|
||||
namespace mocktail::legacy {
|
||||
|
||||
class RuntimeDependencies final {
|
||||
public:
|
||||
using ShutdownBeforePlatformCallback = Status (*)(jnivm::VM* vm);
|
||||
|
||||
RuntimeDependencies() = default;
|
||||
explicit RuntimeDependencies(
|
||||
runtime::AuthRuntimeComposition composition,
|
||||
ShutdownBeforePlatformCallback shutdown_before_platform = nullptr)
|
||||
: jni_vm_(std::move(composition.jni_vm)),
|
||||
account_identity_(std::move(composition.account_identity)),
|
||||
roblox_credential_(std::move(composition.credential)),
|
||||
shutdown_before_platform_(shutdown_before_platform) {}
|
||||
~RuntimeDependencies() {
|
||||
if (jni_vm_ != nullptr && shutdown_before_platform_ != nullptr) {
|
||||
(void)shutdown_before_platform_(jni_vm_.get());
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeDependencies(const RuntimeDependencies&) = delete;
|
||||
RuntimeDependencies& operator=(const RuntimeDependencies&) = delete;
|
||||
RuntimeDependencies(RuntimeDependencies&& other) noexcept
|
||||
: jni_vm_(std::move(other.jni_vm_)),
|
||||
account_identity_(std::move(other.account_identity_)),
|
||||
roblox_credential_(std::move(other.roblox_credential_)),
|
||||
shutdown_before_platform_(
|
||||
std::exchange(other.shutdown_before_platform_, nullptr)) {}
|
||||
RuntimeDependencies& operator=(RuntimeDependencies&& other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
if (jni_vm_ != nullptr && shutdown_before_platform_ != nullptr) {
|
||||
(void)shutdown_before_platform_(jni_vm_.get());
|
||||
}
|
||||
jni_vm_ = std::move(other.jni_vm_);
|
||||
account_identity_ = std::move(other.account_identity_);
|
||||
roblox_credential_ = std::move(other.roblox_credential_);
|
||||
shutdown_before_platform_ =
|
||||
std::exchange(other.shutdown_before_platform_, nullptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
const std::shared_ptr<jnivm::VM>& jni_vm() const { return jni_vm_; }
|
||||
const jnivm::RobloxAuthIdentity& account_identity() const {
|
||||
return account_identity_;
|
||||
}
|
||||
const runtime::SecureRobloxCredential& roblox_credential() const {
|
||||
return roblox_credential_;
|
||||
}
|
||||
|
||||
// Transitional teardown boundary. Runtime-owned subsystems release
|
||||
// SDL resources here after guest workers stop and before window shutdown.
|
||||
Status ShutdownBeforePlatform() {
|
||||
return shutdown_before_platform_ == nullptr
|
||||
? Status::Ok()
|
||||
: shutdown_before_platform_(jni_vm_.get());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<jnivm::VM> jni_vm_;
|
||||
jnivm::RobloxAuthIdentity account_identity_;
|
||||
runtime::SecureRobloxCredential roblox_credential_;
|
||||
ShutdownBeforePlatformCallback shutdown_before_platform_ = nullptr;
|
||||
};
|
||||
|
||||
// Transitional reverse-engineering runtime. New subsystems must expose // interfaces and be composed outside this implementation.
|
||||
int Run(const runtime::CommandLineOptions& options,
|
||||
RuntimeDependencies dependencies);
|
||||
|
||||
} // namespace mocktail::legacy
|
||||
|
||||
#endif // MOCKTAIL_LEGACY_LEGACY_RUNTIME_H_
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright 2026 Sober Test Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// libc_shim/libc_shim.h — Bionic libc → Glibc call translation layer.
|
||||
//
|
||||
// Intercepts calls made by Bionic-compiled code (libroblox.so) to
|
||||
// Bionic-specific libc functions and forwards them to their Glibc equivalents
|
||||
// on the host Linux system.
|
||||
//
|
||||
// Key responsibilities:
|
||||
// - pthread_mutex_t size/layout differences between Bionic and Glibc.
|
||||
// - Virtual filesystem path mapping (APK sandbox → Flatpak paths).
|
||||
// - Bionic-only syscall wrappers missing in Glibc.
|
||||
|
||||
#ifndef SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
#define SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
|
||||
#include <cstdio>
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
namespace libc_shim {
|
||||
|
||||
using GuestAllocator = void* (*)(size_t);
|
||||
|
||||
enum class HostCaBundleStatus {
|
||||
kReady,
|
||||
kInvalidOverride,
|
||||
kUnavailable,
|
||||
};
|
||||
|
||||
struct HostCaBundleResolution {
|
||||
HostCaBundleStatus status = HostCaBundleStatus::kUnavailable;
|
||||
std::string host_path;
|
||||
bool from_override = false;
|
||||
|
||||
bool ok() const { return status == HostCaBundleStatus::kReady; }
|
||||
};
|
||||
|
||||
// Selects the allocator used by libc APIs whose POSIX contract returns an
|
||||
// owned buffer. This keeps allocations on the same side of the host/guest ABI
|
||||
// boundary as the caller that will later free them.
|
||||
void ConfigureGuestAllocator(GuestAllocator allocator) noexcept;
|
||||
|
||||
// Installs all shim hooks. Must be called before the target .so is loaded.
|
||||
//
|
||||
// After this call, dlopen(3) symbol resolution for known Bionic symbols will
|
||||
// be intercepted and redirected to the shim implementations.
|
||||
void Install();
|
||||
|
||||
// Resolves the host TLS trust bundle without weakening certificate
|
||||
// verification. MOCKTAIL_CA_BUNDLE, when present, is authoritative and must
|
||||
// name a readable, non-empty, absolute regular file; an invalid override never
|
||||
// falls back to a system path.
|
||||
HostCaBundleResolution ResolveHostCaBundle();
|
||||
|
||||
// Resolves and registers exact Android CA-file aliases. The aliases are more
|
||||
// specific than the application data prefix and therefore retain the existing
|
||||
// longest-prefix path semantics for every other application path.
|
||||
HostCaBundleResolution ConfigureHostCaBundlePathMappings();
|
||||
|
||||
const char* HostCaBundleStatusName(HostCaBundleStatus status);
|
||||
|
||||
// Maps an Android-style virtual path to its Flatpak sandbox equivalent.
|
||||
//
|
||||
// Example:
|
||||
// "/data/user/0/com.roblox.client/files/logs" →
|
||||
// "/home/user/.var/app/org.vinegarhq.Sober/data/sober/logs"
|
||||
//
|
||||
// Args:
|
||||
// android_path: Absolute path as seen by the Bionic library.
|
||||
//
|
||||
// Returns:
|
||||
// Translated host path. Returns android_path unchanged if no mapping
|
||||
// is registered for it.
|
||||
std::string TranslatePath(const std::string& android_path);
|
||||
|
||||
// Registers a path prefix mapping used by TranslatePath.
|
||||
//
|
||||
// Args:
|
||||
// android_prefix: Android path prefix to intercept.
|
||||
// host_prefix: Replacement prefix on the host filesystem.
|
||||
void RegisterPathMapping(const std::string& android_prefix,
|
||||
const std::string& host_prefix);
|
||||
|
||||
// Removes all registered path mappings. Primarily for use in tests.
|
||||
void ClearPathMappings();
|
||||
|
||||
} // namespace libc_shim
|
||||
|
||||
extern "C" {
|
||||
int mocktail_open(const char* path, int flags, ...);
|
||||
int mocktail___open_2(const char* path, int flags);
|
||||
FILE* mocktail_fopen(const char* path, const char* mode);
|
||||
int mocktail_access(const char* path, int mode);
|
||||
int mocktail_stat(const char* path, struct stat* statbuf);
|
||||
int mocktail_lstat(const char* path, struct stat* statbuf);
|
||||
int mocktail_statvfs(const char* path, struct statvfs* statbuf);
|
||||
int mocktail_statfs(const char* path, struct statfs* statbuf);
|
||||
int mocktail_mkdir(const char* path, mode_t mode);
|
||||
DIR* mocktail_opendir(const char* path);
|
||||
int mocktail_rename(const char* old_path, const char* new_path);
|
||||
int mocktail_unlink(const char* path);
|
||||
int mocktail_rmdir(const char* path);
|
||||
char* mocktail_realpath(const char* path, char* resolved_path);
|
||||
ssize_t mocktail_readlink(const char* path, char* buf, size_t bufsiz);
|
||||
ssize_t mocktail___readlink_chk(const char* path, char* buf, size_t len,
|
||||
size_t buf_len);
|
||||
}
|
||||
|
||||
#endif // SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// linker/linker.h — ELF Bionic-compatible shared library loader.
|
||||
//
|
||||
// Provides a minimal interface for loading Android ELF shared libraries
|
||||
// (.so files compiled for Bionic libc / Android ABI) into a Linux process
|
||||
// address space, resolving dependencies and exporting a symbol table.
|
||||
//
|
||||
// Design notes:
|
||||
// - Uses dlopen(3) under the hood for system libraries.
|
||||
// - For Bionic-ABI libraries (libroblox.so), manual ELF parsing and
|
||||
// mmap(2)-based loading is required; this header exposes that interface.
|
||||
// - All paths follow POSIX conventions.
|
||||
|
||||
#ifndef MOCKTAIL_LINKER_LINKER_H_
|
||||
#define MOCKTAIL_LINKER_LINKER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace linker {
|
||||
|
||||
// Opaque handle returned by LoadLibrary. Callers must not interpret the
|
||||
// pointer value; it is only valid as an argument to ResolveSymbol or
|
||||
// UnloadLibrary.
|
||||
using LibraryHandle = void*;
|
||||
using SymbolMap = std::unordered_map<std::string, void*>;
|
||||
using AndroidLibraryLoadObserver = bool (*)(void* context,
|
||||
std::string_view logical_name,
|
||||
std::uintptr_t image_base);
|
||||
|
||||
struct ProgramHeaderValidation {
|
||||
bool valid = false;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const noexcept { return valid; }
|
||||
};
|
||||
|
||||
// AddSyntheticLibrary — registers one Android SONAME and only the exports
|
||||
// owned by that library. Dependencies of subsequently loaded Android ELF
|
||||
// files resolve these symbols through mcpelauncher-linker.
|
||||
LibraryHandle AddSyntheticLibrary(const std::string& soname,
|
||||
const SymbolMap& exports);
|
||||
|
||||
// RegisterSyntheticSymbol — stages or adds one export to a specific Android
|
||||
// SONAME. If the synthetic library is already loaded, its symbol table is
|
||||
// updated in place.
|
||||
void RegisterSyntheticSymbol(const std::string& soname,
|
||||
const std::string& name, void* address);
|
||||
|
||||
// Registers Android Bionic thread-local and synchronization ABIs only
|
||||
// for synthetic libc.so. Host code keeps its glibc implementations.
|
||||
void RegisterBionicPthreadKeyRuntimeForLibc();
|
||||
|
||||
// Registers Android's DSO-aware __register_atfork entrypoint only for
|
||||
// synthetic libc.so. The host pthread bridge preserves callback order but
|
||||
// cannot unregister callbacks when a guest DSO unloads.
|
||||
void RegisterBionicAtForkRuntimeForLibc();
|
||||
|
||||
// Registers Android LP64 positioned I/O and seek entrypoints only for
|
||||
// synthetic libc.so. The bridge relies on the x86-64 host's 64-bit
|
||||
// off_t contract and never exposes host large-file feature-macro aliases.
|
||||
void RegisterBionicLargeFileRuntimeForLibc();
|
||||
|
||||
// Registers host-independent implementations of Android libc exports that
|
||||
// are not uniformly supplied by glibc and musl. Every symbol remains owned by
|
||||
// synthetic libc.so and preserves the Android x86-64 ABI.
|
||||
void RegisterBionicHostLibcRuntimeForLibc();
|
||||
|
||||
// Registers Bionic sysconf name translation only for synthetic libc.so.
|
||||
// Bionic and glibc use different integer values for sysconf names.
|
||||
void RegisterBionicSysconfRuntimeForLibc();
|
||||
|
||||
// Registers the Android LP64 sigaction layout adapter only for libc.so.
|
||||
void RegisterBionicSignalRuntimeForLibc();
|
||||
|
||||
// Registers the LP64 Bionic FILE/__sF ABI and host stdio translation only for
|
||||
// synthetic libc.so.
|
||||
void RegisterBionicStdioRuntimeForLibc();
|
||||
|
||||
// Restores mcpelauncher-linker's own libdl exports after legacy host-symbol
|
||||
// discovery. Android dlopen/dlsym must never escape into glibc.
|
||||
void RegisterBionicDynamicLoaderForLibdl();
|
||||
|
||||
// GetSyntheticLibrarySymbols — returns a snapshot of one SONAME's exports.
|
||||
// The copy is safe to inspect while other libraries are being registered.
|
||||
SymbolMap GetSyntheticLibrarySymbols(const std::string& soname);
|
||||
|
||||
// UpdateAndroidLibraryPath — configures the Bionic linker's dependency search
|
||||
// path. Multiple directories use the same colon-separated syntax as
|
||||
// LD_LIBRARY_PATH.
|
||||
void UpdateAndroidLibraryPath(const std::string& search_path);
|
||||
|
||||
// Installs the single process owner that validates or adapts a real Android
|
||||
// image immediately after relocation and registration. Returning false from
|
||||
// the observer rejects and unloads that image before startup can call it.
|
||||
bool SetAndroidLibraryLoadObserver(AndroidLibraryLoadObserver observer,
|
||||
void* context);
|
||||
void ClearAndroidLibraryLoadObserver(void* context);
|
||||
|
||||
// OpenAndroidLibrary — loads a real Android ELF and registers its handle under
|
||||
// logical_name. The containing directory is added to the Bionic dependency
|
||||
// search path before loading.
|
||||
LibraryHandle OpenAndroidLibrary(const std::string& real_path,
|
||||
const std::string& logical_name);
|
||||
|
||||
// Legacy compatibility bridge. New code should use AddSyntheticLibrary or
|
||||
// OpenAndroidLibrary so real files and synthetic SONAMEs cannot be confused.
|
||||
LibraryHandle LoadLibrary(const std::string& real_path,
|
||||
const std::string& host_alias);
|
||||
|
||||
// ResolveSymbol — looks up an exported symbol in a loaded library.
|
||||
void* ResolveSymbol(LibraryHandle handle, const std::string& symbol_name);
|
||||
|
||||
// Crashpad execution is intentionally outside Mocktail's supported JNI
|
||||
// surface. These exports are never returned to the host composition even when
|
||||
// a Roblox payload contains them.
|
||||
bool IsBlockedRobloxCrashReportNativeSymbol(
|
||||
std::string_view symbol_name) noexcept;
|
||||
|
||||
// UnloadLibrary — releases resources associated with a loaded library.
|
||||
void UnloadLibrary(LibraryHandle handle);
|
||||
|
||||
// Legacy compatibility bridge used by the current monolithic startup path.
|
||||
// New code should use RegisterSyntheticSymbol and assign every export to its
|
||||
// owning SONAME.
|
||||
void RegisterSymbol(const std::string& name, void* addr);
|
||||
|
||||
// Returns the legacy accumulated symbol table. It remains available until the
|
||||
// existing startup path is migrated to per-SONAME maps.
|
||||
const SymbolMap& GetBionicSymbols();
|
||||
|
||||
// Returns mcpelauncher-linker's Bionic dl_iterate_phdr entrypoint. Android
|
||||
// unwinders must not receive glibc's host implementation/layout.
|
||||
void* BionicDlIteratePhdrAddress();
|
||||
|
||||
// Confirms that the Bionic program-header iterator can see a manually loaded
|
||||
// Android ELF, including the unwind metadata needed for C++ exceptions.
|
||||
ProgramHeaderValidation ValidateBionicProgramHeaders(LibraryHandle handle);
|
||||
|
||||
// Returns the mapped ELF base for a loaded Android library alias. The lookup
|
||||
// is synchronized with library registration and returns zero for an unknown
|
||||
// alias or a host-dlopen library without Bionic image metadata.
|
||||
std::uintptr_t FindLoadedAndroidLibraryBase(const std::string& logical_name);
|
||||
|
||||
// SymbolTable — global registry of alias → handle mappings.
|
||||
const std::unordered_map<std::string, LibraryHandle>& GetSymbolTable();
|
||||
|
||||
} // namespace linker
|
||||
|
||||
#endif // MOCKTAIL_LINKER_LINKER_H_
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_AUDIO_SINK_H_
|
||||
#define MOCKTAIL_AUDIO_AUDIO_SINK_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Interleaved PCM formats accepted at the host audio boundary. Endianness is
|
||||
// explicit because Android x86-64 producers use little-endian PCM regardless
|
||||
// of the host implementation chosen by SDL.
|
||||
enum class PcmSampleFormat {
|
||||
kUnsigned8,
|
||||
kSigned16LittleEndian,
|
||||
kSigned32LittleEndian,
|
||||
kFloat32LittleEndian,
|
||||
};
|
||||
|
||||
struct PcmSpec {
|
||||
int sample_rate_hz = 48000;
|
||||
std::uint8_t channels = 2;
|
||||
PcmSampleFormat format = PcmSampleFormat::kSigned16LittleEndian;
|
||||
};
|
||||
|
||||
using AudioBufferReleaseCallback = void (*)(void* context, const void* data,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// A PCM submission. With no release callback, Enqueue() copies the bytes
|
||||
// before returning. With a release callback, the sink may borrow the buffer;
|
||||
// the caller must keep it valid until the callback runs. A successful
|
||||
// submission invokes the callback exactly once, including when Clear() or
|
||||
// Shutdown() discards queued audio. A failed submission never invokes it.
|
||||
struct PcmBuffer {
|
||||
const void* data = nullptr;
|
||||
std::size_t size_bytes = 0;
|
||||
AudioBufferReleaseCallback release_callback = nullptr;
|
||||
void* release_context = nullptr;
|
||||
};
|
||||
|
||||
Status ValidatePcmSpec(const PcmSpec& spec);
|
||||
std::size_t BytesPerSample(PcmSampleFormat format);
|
||||
std::size_t BytesPerFrame(const PcmSpec& spec);
|
||||
|
||||
// Thread-safe PCM playback contract. Implementations perform device format
|
||||
// conversion and resampling through their host audio library; callers always
|
||||
// submit data in source_spec().
|
||||
class AudioSink {
|
||||
public:
|
||||
virtual ~AudioSink() = default;
|
||||
|
||||
virtual const PcmSpec& source_spec() const = 0;
|
||||
virtual Status Enqueue(const PcmBuffer& buffer) = 0;
|
||||
virtual Status Pause() = 0;
|
||||
virtual Status Resume() = 0;
|
||||
// Linear gain is delegated to the host audio library. Zero mutes, one is
|
||||
// unity gain, and negative or non-finite values are rejected.
|
||||
virtual Status SetGain(float linear_gain) = 0;
|
||||
virtual Status Flush() = 0;
|
||||
virtual Status Clear() = 0;
|
||||
virtual Status GetQueuedBytes(std::size_t* size_bytes) const = 0;
|
||||
|
||||
// Idempotent. It waits for in-flight API calls and releases all borrowed
|
||||
// buffers before returning. Calls started after shutdown fail closed.
|
||||
virtual void Shutdown() = 0;
|
||||
};
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_AUDIO_SINK_H_
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_FMOD_JAVA_AUDIO_RUNTIME_H_
|
||||
#define MOCKTAIL_AUDIO_FMOD_JAVA_AUDIO_RUNTIME_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/audio/audio_sink.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// org.fmod.AudioDevice is an ordinary Java object. The JNI boundary passes
|
||||
// its jobject value through this opaque key without making the audio layer
|
||||
// depend on JNI headers or retain a JNI reference.
|
||||
using FmodJavaAudioDeviceIdentity = const void*;
|
||||
|
||||
// A factory returns a paused sink with exactly source_spec. Production uses
|
||||
// SDL3; tests can inject a deterministic sink without opening a host device.
|
||||
// factory_context must remain valid until the runtime is shut down.
|
||||
using FmodJavaAudioSinkFactory = Status (*)(
|
||||
void* factory_context, const PcmSpec& source_spec,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
|
||||
struct FmodJavaAudioRuntimeOptions {
|
||||
FmodJavaAudioSinkFactory sink_factory = nullptr;
|
||||
void* sink_factory_context = nullptr;
|
||||
|
||||
std::uint32_t max_devices = 8;
|
||||
std::uint32_t max_block_count = 256;
|
||||
std::size_t max_buffer_bytes_per_device = 64U * 1024U * 1024U;
|
||||
};
|
||||
|
||||
// Content-free counters suitable for readiness logs. They never expose PCM,
|
||||
// JNI identities, or guest pointers.
|
||||
struct FmodJavaAudioRuntimeStats {
|
||||
std::uint64_t init_attempts = 0;
|
||||
std::uint64_t initialized_devices = 0;
|
||||
std::uint64_t rejected_init_calls = 0;
|
||||
std::uint64_t close_calls = 0;
|
||||
std::uint64_t rejected_close_calls = 0;
|
||||
std::uint64_t write_attempts = 0;
|
||||
std::uint64_t submitted_buffers = 0;
|
||||
std::uint64_t submitted_bytes = 0;
|
||||
std::uint64_t consumed_buffers = 0;
|
||||
std::uint64_t consumed_bytes = 0;
|
||||
std::uint64_t discarded_buffers = 0;
|
||||
std::uint64_t discarded_bytes = 0;
|
||||
std::uint64_t rejected_write_calls = 0;
|
||||
std::size_t active_devices = 0;
|
||||
std::size_t pending_buffers = 0;
|
||||
std::size_t pending_bytes = 0;
|
||||
};
|
||||
|
||||
// Thread-safe implementation of the APK's exact Java playback contract:
|
||||
//
|
||||
// AudioDevice.init(channels, sampleRate, blockSize, blockCount)Z
|
||||
// AudioDevice.write(byte[], length)V
|
||||
// AudioDevice.close()V
|
||||
//
|
||||
// Init() allocates blockCount fixed slots of blockSize frames. Every successful
|
||||
// Write() copies guest bytes synchronously into one slot, then lends that stable
|
||||
// storage to AudioSink without a second PCM copy. FmodJavaAudioRuntime performs
|
||||
// no per-write allocation for buffer storage or release ownership. A producer
|
||||
// waits with AudioTrack-style blocking backpressure when all slots are borrowed
|
||||
// instead of growing memory or dropping a block.
|
||||
class FmodJavaAudioRuntime final {
|
||||
public:
|
||||
explicit FmodJavaAudioRuntime(
|
||||
const FmodJavaAudioRuntimeOptions& options = {});
|
||||
~FmodJavaAudioRuntime();
|
||||
|
||||
FmodJavaAudioRuntime(const FmodJavaAudioRuntime&) = delete;
|
||||
FmodJavaAudioRuntime& operator=(const FmodJavaAudioRuntime&) = delete;
|
||||
|
||||
Status Init(FmodJavaAudioDeviceIdentity device_identity, int channels,
|
||||
int sample_rate_hz, int block_size_frames, int block_count);
|
||||
Status Write(FmodJavaAudioDeviceIdentity device_identity,
|
||||
const std::uint8_t* bytes, std::size_t size_bytes);
|
||||
Status Close(FmodJavaAudioDeviceIdentity device_identity);
|
||||
|
||||
FmodJavaAudioRuntimeStats GetStats() const;
|
||||
|
||||
// Idempotent. Prevents new devices/writes, shuts down every sink, and waits
|
||||
// for every borrowed owned buffer to be released before returning.
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
std::unique_ptr<State> state_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_FMOD_JAVA_AUDIO_RUNTIME_H_
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_FMOD_JNI_AUDIO_BRIDGE_H_
|
||||
#define MOCKTAIL_AUDIO_FMOD_JNI_AUDIO_BRIDGE_H_
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace jnivm {
|
||||
class VM;
|
||||
} // namespace jnivm
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Installs the SDL-backed org/fmod/AudioDevice implementation into the
|
||||
// pseudo-JVM. The VM retains and shuts down the bridge context.
|
||||
Status InstallFmodJniAudioBridge(jnivm::VM* vm);
|
||||
|
||||
// Stops the Java audio runtime and every SDL stream before the window layer
|
||||
// calls SDL_Quit(). Must run on the main thread after Roblox workers stop.
|
||||
Status ShutdownFmodJniAudioBridge(jnivm::VM* vm);
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_FMOD_JNI_AUDIO_BRIDGE_H_
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_OPENSL_ABI_H_
|
||||
#define MOCKTAIL_AUDIO_OPENSL_ABI_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Prefer the Android NDK's authoritative definitions. Desktop distributions
|
||||
// generally do not ship them, so the fallback below intentionally describes
|
||||
// only the AndroidSimpleBufferQueue ABI used by this adapter. It is not an
|
||||
// OpenSL engine implementation and does not advertise unsupported features.
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
#if !defined(__ANDROID__) && !defined(SL_API_DEPRECATED)
|
||||
// NDK headers annotate deprecation with Clang's Android-only availability
|
||||
// syntax. A host compiler consumes the ABI declarations, not Android API
|
||||
// availability metadata.
|
||||
#define SL_API_DEPRECATED(level)
|
||||
#endif
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <SLES/OpenSLES_Android.h>
|
||||
#define MOCKTAIL_OPENSL_API_ENTRY SLAPIENTRY
|
||||
#endif
|
||||
|
||||
namespace mocktail::audio::opensl_abi {
|
||||
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
|
||||
using Result = SLresult;
|
||||
using Uint32 = SLuint32;
|
||||
using AndroidSimpleBufferQueue = SLAndroidSimpleBufferQueueItf;
|
||||
using AndroidSimpleBufferQueueTable = struct SLAndroidSimpleBufferQueueItf_;
|
||||
using AndroidSimpleBufferQueueState = SLAndroidSimpleBufferQueueState;
|
||||
using AndroidSimpleBufferQueueCallback = slAndroidSimpleBufferQueueCallback;
|
||||
|
||||
inline constexpr Result kResultSuccess = SL_RESULT_SUCCESS;
|
||||
inline constexpr Result kResultPreconditionsViolated =
|
||||
SL_RESULT_PRECONDITIONS_VIOLATED;
|
||||
inline constexpr Result kResultParameterInvalid = SL_RESULT_PARAMETER_INVALID;
|
||||
inline constexpr Result kResultMemoryFailure = SL_RESULT_MEMORY_FAILURE;
|
||||
inline constexpr Result kResultResourceError = SL_RESULT_RESOURCE_ERROR;
|
||||
inline constexpr Result kResultIoError = SL_RESULT_IO_ERROR;
|
||||
inline constexpr Result kResultBufferInsufficient =
|
||||
SL_RESULT_BUFFER_INSUFFICIENT;
|
||||
inline constexpr Result kResultFeatureUnsupported =
|
||||
SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
inline constexpr Result kResultInternalError = SL_RESULT_INTERNAL_ERROR;
|
||||
|
||||
#else
|
||||
|
||||
#define MOCKTAIL_OPENSL_API_ENTRY
|
||||
|
||||
using Result = std::uint32_t;
|
||||
using Uint32 = std::uint32_t;
|
||||
|
||||
inline constexpr Result kResultSuccess = 0;
|
||||
inline constexpr Result kResultPreconditionsViolated = 1;
|
||||
inline constexpr Result kResultParameterInvalid = 2;
|
||||
inline constexpr Result kResultMemoryFailure = 3;
|
||||
inline constexpr Result kResultResourceError = 4;
|
||||
inline constexpr Result kResultIoError = 6;
|
||||
inline constexpr Result kResultBufferInsufficient = 7;
|
||||
inline constexpr Result kResultFeatureUnsupported = 12;
|
||||
inline constexpr Result kResultInternalError = 13;
|
||||
|
||||
struct AndroidSimpleBufferQueueState {
|
||||
Uint32 count;
|
||||
Uint32 index;
|
||||
};
|
||||
|
||||
struct AndroidSimpleBufferQueueTable;
|
||||
using AndroidSimpleBufferQueue = const AndroidSimpleBufferQueueTable* const*;
|
||||
using AndroidSimpleBufferQueueCallback =
|
||||
void (*)(AndroidSimpleBufferQueue caller, void* context);
|
||||
|
||||
struct AndroidSimpleBufferQueueTable {
|
||||
Result (*Enqueue)(AndroidSimpleBufferQueue self, const void* buffer,
|
||||
Uint32 size);
|
||||
Result (*Clear)(AndroidSimpleBufferQueue self);
|
||||
Result (*GetState)(AndroidSimpleBufferQueue self,
|
||||
AndroidSimpleBufferQueueState* state);
|
||||
Result (*RegisterCallback)(AndroidSimpleBufferQueue self,
|
||||
AndroidSimpleBufferQueueCallback callback,
|
||||
void* context);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace mocktail::audio::opensl_abi
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_OPENSL_ABI_H_
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_OPENSL_PLAYBACK_RUNTIME_H_
|
||||
#define MOCKTAIL_AUDIO_OPENSL_PLAYBACK_RUNTIME_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/audio/opensl_runtime_abi.h"
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define MOCKTAIL_OPENSL_EXPORT __attribute__((visibility("default")))
|
||||
#else
|
||||
#define MOCKTAIL_OPENSL_EXPORT
|
||||
#endif
|
||||
|
||||
// Process-wide, content-free evidence from the production OpenSL boundary.
|
||||
// Counters contain no audio samples, filenames, account data, or device names.
|
||||
struct MocktailOpenSlRuntimeStats {
|
||||
std::uint64_t submitted_buffers;
|
||||
std::uint64_t consumed_buffers;
|
||||
std::uint64_t clean_player_shutdowns;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
#if !defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_ANDROIDCONFIGURATION;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_ANDROIDSIMPLEBUFFERQUEUE;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_BUFFERQUEUE;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_ENGINE;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_PLAY;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_RECORD;
|
||||
extern MOCKTAIL_OPENSL_EXPORT const
|
||||
mocktail::audio::opensl_abi::InterfaceId SL_IID_VOLUME;
|
||||
|
||||
MOCKTAIL_OPENSL_EXPORT mocktail::audio::opensl_abi::Result
|
||||
MOCKTAIL_OPENSL_API_ENTRY slCreateEngine(
|
||||
mocktail::audio::opensl_abi::Object* engine,
|
||||
mocktail::audio::opensl_abi::Uint32 num_options,
|
||||
const mocktail::audio::opensl_abi::EngineOption* options,
|
||||
mocktail::audio::opensl_abi::Uint32 num_interfaces,
|
||||
const mocktail::audio::opensl_abi::InterfaceId* interface_ids,
|
||||
const mocktail::audio::opensl_abi::Boolean* interface_required);
|
||||
#endif
|
||||
|
||||
MOCKTAIL_OPENSL_EXPORT mocktail::audio::opensl_abi::Result
|
||||
mocktailOpenSlGetRuntimeStats(MocktailOpenSlRuntimeStats* stats,
|
||||
std::uint32_t stats_size);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_OPENSL_PLAYBACK_RUNTIME_H_
|
||||
@@ -0,0 +1,432 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_OPENSL_RUNTIME_ABI_H_
|
||||
#define MOCKTAIL_AUDIO_OPENSL_RUNTIME_ABI_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/audio/opensl_abi.h"
|
||||
|
||||
// This header describes the Android OpenSL ES subset used by Roblox's embedded
|
||||
// FMOD backend. The layouts and numeric values mirror the NDK ABI. Host audio
|
||||
// policy and playback live in Mocktail classes; no Android implementation
|
||||
// details leak beyond this boundary.
|
||||
namespace mocktail::audio::opensl_abi {
|
||||
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
|
||||
using Boolean = SLboolean;
|
||||
using Int16 = SLint16;
|
||||
using Int32 = SLint32;
|
||||
using Char = SLchar;
|
||||
using Millibel = SLmillibel;
|
||||
using Permille = SLpermille;
|
||||
using InterfaceIdValue = struct SLInterfaceID_;
|
||||
using InterfaceId = SLInterfaceID;
|
||||
using Object = SLObjectItf;
|
||||
using ObjectTable = struct SLObjectItf_;
|
||||
using Engine = SLEngineItf;
|
||||
using EngineTable = struct SLEngineItf_;
|
||||
using Play = SLPlayItf;
|
||||
using PlayTable = struct SLPlayItf_;
|
||||
using Volume = SLVolumeItf;
|
||||
using VolumeTable = struct SLVolumeItf_;
|
||||
using BufferQueue = SLBufferQueueItf;
|
||||
using BufferQueueTable = struct SLBufferQueueItf_;
|
||||
using AndroidConfiguration = SLAndroidConfigurationItf;
|
||||
using AndroidConfigurationTable = struct SLAndroidConfigurationItf_;
|
||||
using ObjectCallback = slObjectCallback;
|
||||
using PlayCallback = slPlayCallback;
|
||||
using DataSource = SLDataSource;
|
||||
using DataSink = SLDataSink;
|
||||
using DataLocatorAndroidSimpleBufferQueue =
|
||||
SLDataLocator_AndroidSimpleBufferQueue;
|
||||
using DataLocatorBufferQueue = SLDataLocator_BufferQueue;
|
||||
using DataLocatorOutputMix = SLDataLocator_OutputMix;
|
||||
using DataFormatPcm = SLDataFormat_PCM;
|
||||
using EngineOption = SLEngineOption;
|
||||
using JavaProxyObject = jobject;
|
||||
|
||||
inline constexpr Result kResultContentUnsupported =
|
||||
SL_RESULT_CONTENT_UNSUPPORTED;
|
||||
inline constexpr Uint32 kObjectStateUnrealized = SL_OBJECT_STATE_UNREALIZED;
|
||||
inline constexpr Uint32 kObjectStateRealized = SL_OBJECT_STATE_REALIZED;
|
||||
inline constexpr Uint32 kObjectStateSuspended = SL_OBJECT_STATE_SUSPENDED;
|
||||
inline constexpr Uint32 kObjectIdEngine = SL_OBJECTID_ENGINE;
|
||||
inline constexpr Uint32 kObjectIdAudioPlayer = SL_OBJECTID_AUDIOPLAYER;
|
||||
inline constexpr Uint32 kObjectIdAudioRecorder = SL_OBJECTID_AUDIORECORDER;
|
||||
inline constexpr Uint32 kObjectIdOutputMix = SL_OBJECTID_OUTPUTMIX;
|
||||
inline constexpr Uint32 kPlayStateStopped = SL_PLAYSTATE_STOPPED;
|
||||
inline constexpr Uint32 kPlayStatePaused = SL_PLAYSTATE_PAUSED;
|
||||
inline constexpr Uint32 kPlayStatePlaying = SL_PLAYSTATE_PLAYING;
|
||||
inline constexpr Uint32 kTimeUnknown = SL_TIME_UNKNOWN;
|
||||
inline constexpr Uint32 kDataLocatorOutputMix = SL_DATALOCATOR_OUTPUTMIX;
|
||||
inline constexpr Uint32 kDataLocatorBufferQueue = SL_DATALOCATOR_BUFFERQUEUE;
|
||||
inline constexpr Uint32 kDataLocatorAndroidSimpleBufferQueue =
|
||||
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
|
||||
inline constexpr Uint32 kDataFormatPcm = SL_DATAFORMAT_PCM;
|
||||
inline constexpr Uint32 kByteOrderBigEndian = SL_BYTEORDER_BIGENDIAN;
|
||||
inline constexpr Uint32 kByteOrderLittleEndian = SL_BYTEORDER_LITTLEENDIAN;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed8 = SL_PCMSAMPLEFORMAT_FIXED_8;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed16 = SL_PCMSAMPLEFORMAT_FIXED_16;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed32 = SL_PCMSAMPLEFORMAT_FIXED_32;
|
||||
inline constexpr Uint32 kSpeakerFrontLeft = SL_SPEAKER_FRONT_LEFT;
|
||||
inline constexpr Uint32 kSpeakerFrontRight = SL_SPEAKER_FRONT_RIGHT;
|
||||
inline constexpr Uint32 kSpeakerFrontCenter = SL_SPEAKER_FRONT_CENTER;
|
||||
inline constexpr Boolean kBooleanFalse = SL_BOOLEAN_FALSE;
|
||||
inline constexpr Boolean kBooleanTrue = SL_BOOLEAN_TRUE;
|
||||
|
||||
#else
|
||||
|
||||
using Boolean = std::uint32_t;
|
||||
using Int16 = std::int16_t;
|
||||
using Int32 = std::int32_t;
|
||||
using Char = std::uint8_t;
|
||||
using Millibel = std::int16_t;
|
||||
using Permille = std::int16_t;
|
||||
|
||||
struct InterfaceIdValue {
|
||||
std::uint32_t time_low;
|
||||
std::uint16_t time_mid;
|
||||
std::uint16_t time_hi_and_version;
|
||||
std::uint16_t clock_seq;
|
||||
std::uint8_t node[6];
|
||||
};
|
||||
using InterfaceId = const InterfaceIdValue*;
|
||||
|
||||
struct ObjectTable;
|
||||
struct EngineTable;
|
||||
struct PlayTable;
|
||||
struct VolumeTable;
|
||||
struct BufferQueueTable;
|
||||
struct AndroidConfigurationTable;
|
||||
|
||||
using Object = const ObjectTable* const*;
|
||||
using Engine = const EngineTable* const*;
|
||||
using Play = const PlayTable* const*;
|
||||
using Volume = const VolumeTable* const*;
|
||||
using BufferQueue = const BufferQueueTable* const*;
|
||||
using AndroidConfiguration = const AndroidConfigurationTable* const*;
|
||||
|
||||
using ObjectCallback = void (*)(Object caller, const void* context,
|
||||
Uint32 event, Result result, Uint32 parameter,
|
||||
void* interface_pointer);
|
||||
using PlayCallback = void (*)(Play caller, void* context, Uint32 event);
|
||||
using BufferQueueCallback = void (*)(BufferQueue caller, void* context);
|
||||
using JavaProxyObject = void*;
|
||||
|
||||
struct DataSource {
|
||||
void* locator;
|
||||
void* format;
|
||||
};
|
||||
|
||||
struct DataSink {
|
||||
void* locator;
|
||||
void* format;
|
||||
};
|
||||
|
||||
struct DataLocatorAndroidSimpleBufferQueue {
|
||||
Uint32 locator_type;
|
||||
Uint32 num_buffers;
|
||||
};
|
||||
|
||||
struct DataLocatorBufferQueue {
|
||||
Uint32 locator_type;
|
||||
Uint32 num_buffers;
|
||||
};
|
||||
|
||||
struct DataLocatorOutputMix {
|
||||
Uint32 locator_type;
|
||||
Object output_mix;
|
||||
};
|
||||
|
||||
struct DataFormatPcm {
|
||||
Uint32 format_type;
|
||||
Uint32 num_channels;
|
||||
Uint32 samples_per_second_millihertz;
|
||||
Uint32 bits_per_sample;
|
||||
Uint32 container_size;
|
||||
Uint32 channel_mask;
|
||||
Uint32 endianness;
|
||||
};
|
||||
|
||||
struct EngineOption {
|
||||
Uint32 feature;
|
||||
Uint32 data;
|
||||
};
|
||||
|
||||
struct ObjectTable {
|
||||
Result (*Realize)(Object self, Boolean async);
|
||||
Result (*Resume)(Object self, Boolean async);
|
||||
Result (*GetState)(Object self, Uint32* state);
|
||||
Result (*GetInterface)(Object self, InterfaceId iid, void* interface_out);
|
||||
Result (*RegisterCallback)(Object self, ObjectCallback callback,
|
||||
void* context);
|
||||
void (*AbortAsyncOperation)(Object self);
|
||||
void (*Destroy)(Object self);
|
||||
Result (*SetPriority)(Object self, Int32 priority, Boolean preemptable);
|
||||
Result (*GetPriority)(Object self, Int32* priority, Boolean* preemptable);
|
||||
Result (*SetLossOfControlInterfaces)(Object self, Int16 num_interfaces,
|
||||
InterfaceId* ids, Boolean enabled);
|
||||
};
|
||||
|
||||
struct EngineTable {
|
||||
Result (*CreateLedDevice)(Engine self, Object* device, Uint32 device_id,
|
||||
Uint32 num_interfaces, const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateVibraDevice)(Engine self, Object* device, Uint32 device_id,
|
||||
Uint32 num_interfaces, const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateAudioPlayer)(Engine self, Object* player, DataSource* source,
|
||||
DataSink* sink, Uint32 num_interfaces,
|
||||
const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateAudioRecorder)(Engine self, Object* recorder,
|
||||
DataSource* source, DataSink* sink,
|
||||
Uint32 num_interfaces, const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateMidiPlayer)(Engine self, Object* player,
|
||||
DataSource* midi_source, DataSource* bank_source,
|
||||
DataSink* audio_output, DataSink* vibra,
|
||||
DataSink* led_array, Uint32 num_interfaces,
|
||||
const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateListener)(Engine self, Object* listener,
|
||||
Uint32 num_interfaces, const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*Create3dGroup)(Engine self, Object* group, Uint32 num_interfaces,
|
||||
const InterfaceId* ids, const Boolean* required);
|
||||
Result (*CreateOutputMix)(Engine self, Object* mix, Uint32 num_interfaces,
|
||||
const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateMetadataExtractor)(Engine self, Object* extractor,
|
||||
DataSource* source, Uint32 num_interfaces,
|
||||
const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*CreateExtensionObject)(Engine self, Object* object,
|
||||
void* parameters, Uint32 object_id,
|
||||
Uint32 num_interfaces,
|
||||
const InterfaceId* ids,
|
||||
const Boolean* required);
|
||||
Result (*QueryNumSupportedInterfaces)(Engine self, Uint32 object_id,
|
||||
Uint32* count);
|
||||
Result (*QuerySupportedInterfaces)(Engine self, Uint32 object_id,
|
||||
Uint32 index, InterfaceId* iid);
|
||||
Result (*QueryNumSupportedExtensions)(Engine self, Uint32* count);
|
||||
Result (*QuerySupportedExtension)(Engine self, Uint32 index, Char* name,
|
||||
Int16* name_length);
|
||||
Result (*IsExtensionSupported)(Engine self, const Char* name,
|
||||
Boolean* supported);
|
||||
};
|
||||
|
||||
struct PlayTable {
|
||||
Result (*SetPlayState)(Play self, Uint32 state);
|
||||
Result (*GetPlayState)(Play self, Uint32* state);
|
||||
Result (*GetDuration)(Play self, Uint32* milliseconds);
|
||||
Result (*GetPosition)(Play self, Uint32* milliseconds);
|
||||
Result (*RegisterCallback)(Play self, PlayCallback callback, void* context);
|
||||
Result (*SetCallbackEventsMask)(Play self, Uint32 event_flags);
|
||||
Result (*GetCallbackEventsMask)(Play self, Uint32* event_flags);
|
||||
Result (*SetMarkerPosition)(Play self, Uint32 milliseconds);
|
||||
Result (*ClearMarkerPosition)(Play self);
|
||||
Result (*GetMarkerPosition)(Play self, Uint32* milliseconds);
|
||||
Result (*SetPositionUpdatePeriod)(Play self, Uint32 milliseconds);
|
||||
Result (*GetPositionUpdatePeriod)(Play self, Uint32* milliseconds);
|
||||
};
|
||||
|
||||
struct VolumeTable {
|
||||
Result (*SetVolumeLevel)(Volume self, Millibel level);
|
||||
Result (*GetVolumeLevel)(Volume self, Millibel* level);
|
||||
Result (*GetMaxVolumeLevel)(Volume self, Millibel* level);
|
||||
Result (*SetMute)(Volume self, Boolean mute);
|
||||
Result (*GetMute)(Volume self, Boolean* mute);
|
||||
Result (*EnableStereoPosition)(Volume self, Boolean enable);
|
||||
Result (*IsEnabledStereoPosition)(Volume self, Boolean* enabled);
|
||||
Result (*SetStereoPosition)(Volume self, Permille position);
|
||||
Result (*GetStereoPosition)(Volume self, Permille* position);
|
||||
};
|
||||
|
||||
struct BufferQueueState {
|
||||
Uint32 count;
|
||||
Uint32 play_index;
|
||||
};
|
||||
|
||||
struct BufferQueueTable {
|
||||
Result (*Enqueue)(BufferQueue self, const void* buffer, Uint32 size);
|
||||
Result (*Clear)(BufferQueue self);
|
||||
Result (*GetState)(BufferQueue self, BufferQueueState* state);
|
||||
Result (*RegisterCallback)(BufferQueue self, BufferQueueCallback callback,
|
||||
void* context);
|
||||
};
|
||||
|
||||
struct AndroidConfigurationTable {
|
||||
Result (*SetConfiguration)(AndroidConfiguration self, const Char* key,
|
||||
const void* value, Uint32 value_size);
|
||||
Result (*GetConfiguration)(AndroidConfiguration self, const Char* key,
|
||||
Uint32* value_size, void* value);
|
||||
Result (*AcquireJavaProxy)(AndroidConfiguration self, Uint32 proxy_type,
|
||||
JavaProxyObject* proxy_object);
|
||||
Result (*ReleaseJavaProxy)(AndroidConfiguration self, Uint32 proxy_type);
|
||||
};
|
||||
|
||||
inline constexpr Result kResultContentUnsupported = 9;
|
||||
inline constexpr Uint32 kObjectStateUnrealized = 1;
|
||||
inline constexpr Uint32 kObjectStateRealized = 2;
|
||||
inline constexpr Uint32 kObjectStateSuspended = 3;
|
||||
inline constexpr Uint32 kObjectIdEngine = 0x00001001;
|
||||
inline constexpr Uint32 kObjectIdAudioPlayer = 0x00001004;
|
||||
inline constexpr Uint32 kObjectIdAudioRecorder = 0x00001005;
|
||||
inline constexpr Uint32 kObjectIdOutputMix = 0x00001009;
|
||||
inline constexpr Uint32 kPlayStateStopped = 1;
|
||||
inline constexpr Uint32 kPlayStatePaused = 2;
|
||||
inline constexpr Uint32 kPlayStatePlaying = 3;
|
||||
inline constexpr Uint32 kTimeUnknown = 0xffffffffU;
|
||||
inline constexpr Uint32 kDataLocatorOutputMix = 0x00000004;
|
||||
inline constexpr Uint32 kDataLocatorBufferQueue = 0x00000006;
|
||||
inline constexpr Uint32 kDataLocatorAndroidSimpleBufferQueue = 0x800007bdU;
|
||||
inline constexpr Uint32 kDataFormatPcm = 0x00000002;
|
||||
inline constexpr Uint32 kByteOrderBigEndian = 1;
|
||||
inline constexpr Uint32 kByteOrderLittleEndian = 2;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed8 = 8;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed16 = 16;
|
||||
inline constexpr Uint32 kPcmSampleFormatFixed32 = 32;
|
||||
inline constexpr Uint32 kSpeakerFrontLeft = 0x00000001;
|
||||
inline constexpr Uint32 kSpeakerFrontRight = 0x00000002;
|
||||
inline constexpr Uint32 kSpeakerFrontCenter = 0x00000004;
|
||||
inline constexpr Boolean kBooleanFalse = 0;
|
||||
inline constexpr Boolean kBooleanTrue = 1;
|
||||
|
||||
#endif
|
||||
|
||||
inline const void* DataSourceLocator(const DataSource& source) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return source.pLocator;
|
||||
#else
|
||||
return source.locator;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline const void* DataSourceFormat(const DataSource& source) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return source.pFormat;
|
||||
#else
|
||||
return source.format;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline const void* DataSinkLocator(const DataSink& sink) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return sink.pLocator;
|
||||
#else
|
||||
return sink.locator;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 QueueLocatorType(
|
||||
const DataLocatorAndroidSimpleBufferQueue& locator) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return locator.locatorType;
|
||||
#else
|
||||
return locator.locator_type;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 QueueLocatorCapacity(
|
||||
const DataLocatorAndroidSimpleBufferQueue& locator) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return locator.numBuffers;
|
||||
#else
|
||||
return locator.num_buffers;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 OutputMixLocatorType(const DataLocatorOutputMix& locator) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return locator.locatorType;
|
||||
#else
|
||||
return locator.locator_type;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Object OutputMixObject(const DataLocatorOutputMix& locator) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return locator.outputMix;
|
||||
#else
|
||||
return locator.output_mix;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmFormatType(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.formatType;
|
||||
#else
|
||||
return format.format_type;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmChannelCount(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.numChannels;
|
||||
#else
|
||||
return format.num_channels;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmSampleRateMillihertz(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.samplesPerSec;
|
||||
#else
|
||||
return format.samples_per_second_millihertz;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmBitsPerSample(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.bitsPerSample;
|
||||
#else
|
||||
return format.bits_per_sample;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmContainerSize(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.containerSize;
|
||||
#else
|
||||
return format.container_size;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmChannelMask(const DataFormatPcm& format) {
|
||||
#if defined(MOCKTAIL_USE_SYSTEM_OPENSL_HEADERS)
|
||||
return format.channelMask;
|
||||
#else
|
||||
return format.channel_mask;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline Uint32 PcmEndianness(const DataFormatPcm& format) {
|
||||
return format.endianness;
|
||||
}
|
||||
|
||||
inline constexpr const char kAndroidPlaybackStreamTypeKey[] =
|
||||
"androidPlaybackStreamType";
|
||||
inline constexpr const char kAndroidPerformanceModeKey[] =
|
||||
"androidPerformanceMode";
|
||||
inline constexpr Int32 kAndroidStreamMedia = 3;
|
||||
inline constexpr Uint32 kAndroidPerformanceNone = 0;
|
||||
|
||||
} // namespace mocktail::audio::opensl_abi
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_OPENSL_RUNTIME_ABI_H_
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_OPENSL_SIMPLE_BUFFER_QUEUE_H_
|
||||
#define MOCKTAIL_AUDIO_OPENSL_SIMPLE_BUFFER_QUEUE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/audio/audio_sink.h"
|
||||
#include "mocktail/audio/opensl_abi.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
enum class OpenSlBufferQueueEvent {
|
||||
kSubmitted,
|
||||
kConsumed,
|
||||
kDiscarded,
|
||||
};
|
||||
|
||||
using OpenSlBufferQueueEventCallback = void (*)(
|
||||
void* context, OpenSlBufferQueueEvent event, std::size_t size_bytes);
|
||||
|
||||
struct OpenSlSimpleBufferQueueOptions {
|
||||
std::uint32_t max_buffers = 8;
|
||||
OpenSlBufferQueueEventCallback event_callback = nullptr;
|
||||
void* event_context = nullptr;
|
||||
};
|
||||
|
||||
struct OpenSlSimpleBufferQueueStats {
|
||||
std::uint64_t submitted_buffers = 0;
|
||||
std::uint64_t consumed_buffers = 0;
|
||||
std::uint64_t discarded_buffers = 0;
|
||||
std::uint64_t consumed_bytes = 0;
|
||||
std::size_t pending_buffers = 0;
|
||||
};
|
||||
|
||||
// Minimal AndroidSimpleBufferQueue-to-AudioSink adapter. It does
|
||||
// not fabricate an OpenSL engine, mixer, recorder, or resampler. The adapter
|
||||
// owns the sink, retains each borrowed OpenSL buffer until the sink releases
|
||||
// it, and dispatches OpenSL callbacks outside all queue/device locks.
|
||||
class OpenSlSimpleBufferQueueAdapter final {
|
||||
public:
|
||||
static Status Create(
|
||||
std::unique_ptr<AudioSink> sink,
|
||||
const OpenSlSimpleBufferQueueOptions& options,
|
||||
std::unique_ptr<OpenSlSimpleBufferQueueAdapter>* adapter);
|
||||
|
||||
~OpenSlSimpleBufferQueueAdapter();
|
||||
|
||||
OpenSlSimpleBufferQueueAdapter(const OpenSlSimpleBufferQueueAdapter&) =
|
||||
delete;
|
||||
OpenSlSimpleBufferQueueAdapter& operator=(
|
||||
const OpenSlSimpleBufferQueueAdapter&) = delete;
|
||||
|
||||
opensl_abi::AndroidSimpleBufferQueue interface() const;
|
||||
Status SetPlaying(bool playing);
|
||||
Status SetGain(float linear_gain);
|
||||
OpenSlSimpleBufferQueueStats GetStats() const;
|
||||
Status last_error() const;
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
|
||||
explicit OpenSlSimpleBufferQueueAdapter(std::shared_ptr<State> state);
|
||||
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
|
||||
// Capture is outside the current compatibility boundary. Callers get an
|
||||
// explicit unsupported result instead of a successful recorder-shaped no-op.
|
||||
Status OpenSlRecorderSupportStatus();
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_OPENSL_SIMPLE_BUFFER_QUEUE_H_
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_ROBLOX_OUTPUT_DEVICE_BRIDGE_H_
|
||||
#define MOCKTAIL_AUDIO_ROBLOX_OUTPUT_DEVICE_BRIDGE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "compat/build_profile.h"
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Replaces only the output-device query/select slots of the exact
|
||||
// Build-ID-scoped FmodAudioDevice vtable. Roblox keeps owning its FMOD engine,
|
||||
// while the existing settings UI sees and selects the SDL host routes that
|
||||
// consume Android AudioTrack PCM.
|
||||
class RobloxOutputDeviceBridge final {
|
||||
public:
|
||||
RobloxOutputDeviceBridge() = default;
|
||||
~RobloxOutputDeviceBridge();
|
||||
|
||||
RobloxOutputDeviceBridge(const RobloxOutputDeviceBridge&) = delete;
|
||||
RobloxOutputDeviceBridge& operator=(const RobloxOutputDeviceBridge&) = delete;
|
||||
|
||||
Status Install(const compat::BuildProfile& profile);
|
||||
void Shutdown();
|
||||
bool installed() const { return installed_; }
|
||||
bool active() const { return active_; }
|
||||
|
||||
private:
|
||||
struct MenuDevice {
|
||||
std::uint32_t playback_device_id = 0;
|
||||
std::string name;
|
||||
std::string guid;
|
||||
};
|
||||
|
||||
static bool ObserveAndroidLibrary(void* context,
|
||||
std::string_view logical_name,
|
||||
std::uintptr_t image_base);
|
||||
static int GetOutputDeviceCount(void* self);
|
||||
static void* GetOutputDeviceInfo(void* result, void* self, int index);
|
||||
static int GetCurrentOutputDevice(void* self);
|
||||
static void SetCurrentOutputDevice(void* self, int index);
|
||||
|
||||
Status Activate(std::uintptr_t image_base);
|
||||
Status PatchVtableLocked();
|
||||
bool RestoreVtableLocked();
|
||||
int DeviceCount();
|
||||
void* DeviceInfo(void* result, int index);
|
||||
int CurrentDevice();
|
||||
void SelectDevice(int index);
|
||||
void ConstructGuestString(void* destination, std::string_view value) const;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
compat::FmodOutputDeviceBridgeProfile profile_;
|
||||
std::vector<MenuDevice> devices_;
|
||||
std::array<std::uintptr_t, 4> original_methods_{};
|
||||
std::uintptr_t library_base_ = 0;
|
||||
std::uintptr_t* vtable_ = nullptr;
|
||||
void* string_constructor_ = nullptr;
|
||||
int selected_index_ = 0;
|
||||
bool installed_ = false;
|
||||
bool active_ = false;
|
||||
};
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_ROBLOX_OUTPUT_DEVICE_BRIDGE_H_
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_AUDIO_SDL_AUDIO_SINK_H_
|
||||
#define MOCKTAIL_AUDIO_SDL_AUDIO_SINK_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/audio/audio_sink.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// SDL documents subsystem initialization and shutdown as main-thread-only.
|
||||
// The process composition root owns this lifetime; audio workers only create
|
||||
// streams while the subsystem is active.
|
||||
Status InitializeSdlAudioSubsystem();
|
||||
Status ShutdownSdlAudioSubsystem();
|
||||
|
||||
struct SdlPlaybackDevice {
|
||||
std::uint32_t id = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Lists physical SDL playback devices. The abstract `default` target is not a
|
||||
// physical entry and is therefore intentionally absent from this vector.
|
||||
Status ListSdlPlaybackDevices(std::vector<SdlPlaybackDevice>* devices);
|
||||
|
||||
// Resolves `default` or one exact physical device name. This pure selection
|
||||
// step is separate from SDL enumeration so configuration behavior is directly
|
||||
// testable.
|
||||
Status ResolveSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
const std::vector<SdlPlaybackDevice>& available_devices,
|
||||
std::uint32_t* playback_device_id, std::string* resolved_name);
|
||||
|
||||
// Sets the process playback target used by every subsequently created FMOD
|
||||
// Java and OpenSL sink. It must run after SDL audio initialization and before
|
||||
// the first sink is opened.
|
||||
Status ConfigureSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
std::vector<SdlPlaybackDevice>* available_devices,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Returns the process playback target currently used by new sinks. The
|
||||
// physical ID is zero for SDL's system-default route.
|
||||
Status GetConfiguredSdlPlaybackDevice(std::uint32_t* playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Atomically migrates every live FMOD Java and OpenSL stream to one physical
|
||||
// SDL device, or to the system default when playback_device_id is zero. New
|
||||
// sinks inherit the same target. Existing queued PCM stays in its stream.
|
||||
Status SwitchSdlPlaybackDevice(std::uint32_t playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
struct SdlAudioSinkOptions {
|
||||
PcmSpec source_spec;
|
||||
|
||||
// Zero selects the process playback target configured above (the SDL
|
||||
// default until explicitly configured). A nonzero ID bypasses that target.
|
||||
std::uint32_t playback_device_id = 0;
|
||||
bool start_paused = true;
|
||||
};
|
||||
|
||||
// Opens a real SDL 3.4+ playback device and its bound SDL_AudioStream. The SDL
|
||||
// audio subsystem must already be owned by InitializeSdlAudioSubsystem(). SDL
|
||||
// owns all format conversion, resampling, and borrowed-buffer consumption.
|
||||
// On failure, *sink remains null.
|
||||
Status CreateSdlAudioSink(const SdlAudioSinkOptions& options,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_SDL_AUDIO_SINK_H_
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_ANDROID_VULKAN_WSI_ADAPTER_H_
|
||||
#define MOCKTAIL_GRAPHICS_ANDROID_VULKAN_WSI_ADAPTER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/graphics/vulkan_abi.h"
|
||||
#include "mocktail/graphics/sdl_vulkan_wsi.h"
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Vulkan VkResult values are a stable signed 32-bit ABI. Keeping the Android
|
||||
// boundary independent of the optional Vulkan SDK lets capability-only builds
|
||||
// continue to compile with SDL's opaque Vulkan handles.
|
||||
using AndroidVulkanResult = VkResult;
|
||||
|
||||
inline constexpr AndroidVulkanResult kAndroidVulkanSuccess =
|
||||
static_cast<AndroidVulkanResult>(0);
|
||||
inline constexpr AndroidVulkanResult kAndroidVulkanErrorInitializationFailed =
|
||||
static_cast<AndroidVulkanResult>(-3);
|
||||
inline constexpr std::uint64_t kHostInfiniteWaitDiagnosticSliceNs =
|
||||
1000000000ULL;
|
||||
inline constexpr std::uint64_t kHostImageAcquireWatchdogTimeoutNs =
|
||||
kHostInfiniteWaitDiagnosticSliceNs;
|
||||
|
||||
inline constexpr char kAndroidSurfaceExtension[] =
|
||||
"VK_KHR_android_surface";
|
||||
|
||||
// Desktop compositors can report a valid acquired/presented image as
|
||||
// suboptimal while a fullscreen configure settles. The Android client path
|
||||
// used by Roblox treats that positive success code as a hard error, so the
|
||||
// compatibility boundary exposes it as the equivalent VK_SUCCESS result.
|
||||
VkResult NormalizeAndroidSwapchainResult(VkResult result);
|
||||
|
||||
// Android requests an infinite image-acquire wait, but a lost Wayland/X11
|
||||
// compositor wakeup would then block Roblox's RenderJob forever. Only the
|
||||
// infinite guest request is bounded; explicit finite Vulkan timeouts retain
|
||||
// their original contract.
|
||||
std::uint64_t BoundHostImageAcquireTimeout(std::uint64_t requested_timeout);
|
||||
bool IsHostImageAcquireWatchdogTimeout(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
VkResult NormalizeHostImageAcquireResult(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Infinite fence/timeline waits retain their guest-visible semantics, but are
|
||||
// issued to the host in finite slices so a stalled renderer produces evidence
|
||||
// while it remains blocked. VK_TIMEOUT from a slice is never returned to the
|
||||
// Android client as a completed wait.
|
||||
std::uint64_t BoundHostSynchronizationWaitTimeout(
|
||||
std::uint64_t requested_timeout);
|
||||
bool ShouldContinueHostSynchronizationWait(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Rewrites an Android VkInstance extension request for the host WSI. The
|
||||
// returned storage owns its strings and can be used to build the
|
||||
// ppEnabledExtensionNames array passed to the host vkCreateInstance call.
|
||||
Status TranslateAndroidVulkanInstanceExtensions(
|
||||
const std::vector<std::string>& android_extensions,
|
||||
const std::vector<std::string>& host_wsi_extensions,
|
||||
std::vector<std::string>* host_extensions);
|
||||
|
||||
// Android WSI boundary. vkCreateAndroidSurfaceKHR-compatible exports can
|
||||
// delegate here after vkCreateInstance has used the translated extension list.
|
||||
// The Android native-window member is intentionally ignored: SDL owns the real
|
||||
// host window and is the only component allowed to create its VkSurfaceKHR.
|
||||
class AndroidVulkanWsiAdapter final {
|
||||
public:
|
||||
using PresentCallback = void (*)(void* user_data);
|
||||
|
||||
Status Initialize(SdlVulkanWsi* host_wsi,
|
||||
PresentCallback present_callback = nullptr,
|
||||
void* present_callback_data = nullptr);
|
||||
void Shutdown();
|
||||
bool IsInitialized() const { return host_wsi_ != nullptr; }
|
||||
|
||||
Status TranslateInstanceExtensions(
|
||||
const std::vector<std::string>& android_extensions,
|
||||
std::vector<std::string>* host_extensions) const;
|
||||
|
||||
AndroidVulkanResult CreateAndroidSurface(
|
||||
VkInstance instance, const void* android_create_info,
|
||||
const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface) const;
|
||||
void DestroySurface(VkInstance instance, VkSurfaceKHR surface,
|
||||
const VkAllocationCallbacks* allocator) const;
|
||||
|
||||
VkResult EnumerateInstanceExtensionProperties(
|
||||
const char* layer_name, std::uint32_t* property_count,
|
||||
VkExtensionProperties* properties) const;
|
||||
VkResult CreateInstance(const VkInstanceCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkInstance* instance);
|
||||
PFN_vkVoidFunction GetInstanceProcAddress(VkInstance instance,
|
||||
const char* name);
|
||||
PFN_vkVoidFunction GetDeviceProcAddress(VkDevice device, const char* name);
|
||||
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info);
|
||||
|
||||
private:
|
||||
SdlVulkanWsi* host_wsi_ = nullptr;
|
||||
PFN_vkGetInstanceProcAddr host_get_instance_proc_address_ = nullptr;
|
||||
PFN_vkGetDeviceProcAddr host_get_device_proc_address_ = nullptr;
|
||||
PFN_vkQueuePresentKHR host_queue_present_ = nullptr;
|
||||
PresentCallback present_callback_ = nullptr;
|
||||
void* present_callback_data_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ANDROID_VULKAN_WSI_ADAPTER_H_
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_ANGLE_PROBE_H_
|
||||
#define MOCKTAIL_GRAPHICS_ANGLE_PROBE_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/graphics/graphics_backend.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
struct AngleProbeOptions {
|
||||
// Production callers provide paths from one pinned ANGLE distribution.
|
||||
// This probe intentionally does not scan browser installation directories.
|
||||
std::string egl_library_path;
|
||||
std::string gles_library_path;
|
||||
bool allow_software_device = false;
|
||||
};
|
||||
|
||||
// Loads a pinned ANGLE pair, verifies the required EGL/GLES symbols, and
|
||||
// initializes an EGL_PLATFORM_ANGLE Vulkan display. No context is fabricated.
|
||||
BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options);
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ANGLE_PROBE_H_
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Exact-path loader for Mocktail's Bionic libEGL adapter.
|
||||
//
|
||||
// A host EGL implementation may have the Android SONAME "libEGL.so" too
|
||||
// (Chromium ANGLE does). Loading by SONAME after SDL/ANGLE starts can then
|
||||
// return the host implementation and make Android code create a second EGL
|
||||
// surface for one native window. This adapter opens the Mocktail
|
||||
// bridge beside the executable and exposes only its EGL exports for the
|
||||
// synthetic Bionic libEGL.so dependency.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
#define MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
using EglExportMap = std::unordered_map<std::string, void *>;
|
||||
|
||||
class BionicEglBridge {
|
||||
public:
|
||||
BionicEglBridge() = default;
|
||||
BionicEglBridge(const BionicEglBridge &) = delete;
|
||||
BionicEglBridge &operator=(const BionicEglBridge &) = delete;
|
||||
|
||||
// Loads $ORIGIN/libEGL.so by its exact absolute path. On success, the
|
||||
// handle intentionally remains live for the process lifetime because the
|
||||
// Bionic linker retains the exported function addresses.
|
||||
bool Load();
|
||||
|
||||
bool IsLoaded() const { return handle_ != nullptr; }
|
||||
void *handle() const { return handle_; }
|
||||
const EglExportMap &exports() const { return exports_; }
|
||||
const std::string &library_path() const { return library_path_; }
|
||||
const std::string &error() const { return error_; }
|
||||
|
||||
private:
|
||||
void *handle_ = nullptr;
|
||||
EglExportMap exports_;
|
||||
std::string library_path_;
|
||||
std::string error_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_GRAPHICS_BACKEND_H_
|
||||
#define MOCKTAIL_GRAPHICS_GRAPHICS_BACKEND_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
enum class GraphicsBackendKind {
|
||||
kAngleVulkan = 0,
|
||||
kDirectVulkan,
|
||||
kSystemEgl,
|
||||
};
|
||||
|
||||
enum class CapabilityState {
|
||||
kUnavailable = 0,
|
||||
// Loader and WSI entry points exist, but no device/surface was validated.
|
||||
kLoadable,
|
||||
// A real API display/device initialization completed successfully.
|
||||
kReady,
|
||||
};
|
||||
|
||||
enum class HardwareAcceleration {
|
||||
kUnknown = 0,
|
||||
kSoftware,
|
||||
kHardware,
|
||||
};
|
||||
|
||||
struct BackendCapability {
|
||||
GraphicsBackendKind backend = GraphicsBackendKind::kAngleVulkan;
|
||||
CapabilityState state = CapabilityState::kUnavailable;
|
||||
HardwareAcceleration acceleration = HardwareAcceleration::kUnknown;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
struct BackendSelectionPolicy {
|
||||
GraphicsBackendKind requested = GraphicsBackendKind::kAngleVulkan;
|
||||
|
||||
// Production remains fail-closed. Fallback must be an explicit policy.
|
||||
bool allow_fallback = false;
|
||||
bool require_hardware_acceleration = true;
|
||||
bool require_runtime_validation = true;
|
||||
};
|
||||
|
||||
struct BackendSelection {
|
||||
Status status;
|
||||
GraphicsBackendKind backend = GraphicsBackendKind::kAngleVulkan;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
BackendSelection SelectGraphicsBackend(
|
||||
const BackendSelectionPolicy& policy,
|
||||
const std::vector<BackendCapability>& capabilities);
|
||||
|
||||
const char* GraphicsBackendName(GraphicsBackendKind backend);
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_GRAPHICS_BACKEND_H_
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
|
||||
#define MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
enum class PresentModePolicy {
|
||||
kHostDefault,
|
||||
kVsync,
|
||||
kUnthrottled,
|
||||
};
|
||||
|
||||
PresentModePolicy ResolvePresentModePolicy(std::string_view vsync_value,
|
||||
std::string_view frame_rate_value);
|
||||
std::vector<VkPresentModeKHR>
|
||||
FilterPresentModes(PresentModePolicy policy,
|
||||
const std::vector<VkPresentModeKHR> &host_modes);
|
||||
const char *PresentModePolicyName(PresentModePolicy policy);
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_SDL_VULKAN_WSI_H_
|
||||
#define MOCKTAIL_GRAPHICS_SDL_VULKAN_WSI_H_
|
||||
|
||||
#if __has_include(<vulkan/vulkan.h>)
|
||||
#include <vulkan/vulkan.h>
|
||||
#endif
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/graphics/graphics_backend.h"
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Thin host WSI scaffold. It does not create a VkInstance, device, swapchain,
|
||||
// renderer, or allocator. A future Android Vulkan adapter may translate
|
||||
// vkCreateAndroidSurfaceKHR into CreateSurface after rewriting extensions.
|
||||
class SdlVulkanWsi final {
|
||||
public:
|
||||
SdlVulkanWsi() = default;
|
||||
~SdlVulkanWsi();
|
||||
|
||||
SdlVulkanWsi(const SdlVulkanWsi&) = delete;
|
||||
SdlVulkanWsi& operator=(const SdlVulkanWsi&) = delete;
|
||||
|
||||
Status Initialize(const platform::NativeWindowDescriptor& window,
|
||||
const std::string& vulkan_loader_path = std::string());
|
||||
void Shutdown();
|
||||
bool IsInitialized() const { return initialized_; }
|
||||
|
||||
const std::vector<std::string>& required_instance_extensions() const {
|
||||
return required_instance_extensions_;
|
||||
}
|
||||
|
||||
SDL_FunctionPointer GetInstanceProcAddress() const;
|
||||
BackendCapability DescribeCapability() const;
|
||||
Status CreateSurface(VkInstance instance,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkSurfaceKHR* surface) const;
|
||||
void DestroySurface(VkInstance instance, VkSurfaceKHR surface,
|
||||
const VkAllocationCallbacks* allocator) const;
|
||||
|
||||
private:
|
||||
void* sdl_window_ = nullptr;
|
||||
std::vector<std::string> required_instance_extensions_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_SDL_VULKAN_WSI_H_
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_TEXT_OVERLAY_FRAME_H_
|
||||
#define MOCKTAIL_GRAPHICS_TEXT_OVERLAY_FRAME_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Stable process-local ABI between the host input runtime in the executable
|
||||
// and libvulkan.so, which is loaded as a separate Android compatibility DSO.
|
||||
// Pixels are tightly packed RGBA8. The producer owns and securely clears its
|
||||
// storage; consumers must copy a matching revision before returning.
|
||||
struct MocktailTextOverlayFrameInfo {
|
||||
static constexpr std::uint32_t kAbiVersion = 2;
|
||||
|
||||
std::uint32_t abi_version = kAbiVersion;
|
||||
std::uint32_t visible = 0;
|
||||
std::uint64_t revision = 0;
|
||||
// x/y/width/height use the host's logical input coordinate space. These
|
||||
// dimensions let a Vulkan consumer map that space to the swapchain's pixel
|
||||
// extent without assuming a 1:1 high-DPI scale.
|
||||
std::uint32_t coordinate_width = 0;
|
||||
std::uint32_t coordinate_height = 0;
|
||||
std::int32_t x = 0;
|
||||
std::int32_t y = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t row_bytes = 0;
|
||||
std::uint64_t rgba_bytes = 0;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Lock-free conservative hint for the Vulkan present fast path. A false
|
||||
// result guarantees that no drawable overlay is currently published. A true
|
||||
// result only means that the synchronized query/copy path must decide.
|
||||
bool mocktail_text_overlay_may_present();
|
||||
bool mocktail_text_overlay_query(MocktailTextOverlayFrameInfo* frame);
|
||||
bool mocktail_text_overlay_copy(std::uint64_t revision, void* rgba,
|
||||
std::size_t rgba_capacity);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_TEXT_OVERLAY_FRAME_H_
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_VULKAN_ABI_H_
|
||||
#define MOCKTAIL_GRAPHICS_VULKAN_ABI_H_
|
||||
|
||||
#if __has_include(<vulkan/vulkan.h>)
|
||||
#include <vulkan/vulkan.h>
|
||||
#else
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#define VULKAN_CORE_H_ 1
|
||||
#define VKAPI_ATTR
|
||||
#define VKAPI_CALL
|
||||
#define VKAPI_PTR VKAPI_CALL
|
||||
#define VK_MAX_EXTENSION_NAME_SIZE 256
|
||||
|
||||
using VkFlags = std::uint32_t;
|
||||
using VkResult = std::int32_t;
|
||||
using VkStructureType = std::int32_t;
|
||||
using PFN_vkVoidFunction = void(VKAPI_PTR*)();
|
||||
|
||||
#define VK_DEFINE_HANDLE(object) typedef struct object##_T* object;
|
||||
#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) \
|
||||
typedef struct object##_T* object;
|
||||
VK_DEFINE_HANDLE(VkInstance)
|
||||
VK_DEFINE_HANDLE(VkPhysicalDevice)
|
||||
VK_DEFINE_HANDLE(VkDevice)
|
||||
VK_DEFINE_HANDLE(VkQueue)
|
||||
VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSurfaceKHR)
|
||||
#undef VK_DEFINE_HANDLE
|
||||
#undef VK_DEFINE_NON_DISPATCHABLE_HANDLE
|
||||
|
||||
struct VkAllocationCallbacks;
|
||||
struct VkPresentInfoKHR;
|
||||
|
||||
struct VkInstanceCreateInfo {
|
||||
VkStructureType sType;
|
||||
const void* pNext;
|
||||
VkFlags flags;
|
||||
const void* pApplicationInfo;
|
||||
std::uint32_t enabledLayerCount;
|
||||
const char* const* ppEnabledLayerNames;
|
||||
std::uint32_t enabledExtensionCount;
|
||||
const char* const* ppEnabledExtensionNames;
|
||||
};
|
||||
|
||||
struct VkExtensionProperties {
|
||||
char extensionName[VK_MAX_EXTENSION_NAME_SIZE];
|
||||
std::uint32_t specVersion;
|
||||
};
|
||||
|
||||
using PFN_vkGetInstanceProcAddr = PFN_vkVoidFunction(VKAPI_PTR*)(
|
||||
VkInstance, const char*);
|
||||
using PFN_vkGetDeviceProcAddr = PFN_vkVoidFunction(VKAPI_PTR*)(VkDevice,
|
||||
const char*);
|
||||
using PFN_vkCreateInstance = VkResult(VKAPI_PTR*)(
|
||||
const VkInstanceCreateInfo*, const VkAllocationCallbacks*, VkInstance*);
|
||||
using PFN_vkEnumerateInstanceExtensionProperties = VkResult(VKAPI_PTR*)(
|
||||
const char*, std::uint32_t*, VkExtensionProperties*);
|
||||
using PFN_vkQueuePresentKHR = VkResult(VKAPI_PTR*)(VkQueue,
|
||||
const VkPresentInfoKHR*);
|
||||
|
||||
inline constexpr VkResult VK_SUCCESS = 0;
|
||||
inline constexpr VkResult VK_TIMEOUT = 2;
|
||||
inline constexpr VkResult VK_INCOMPLETE = 5;
|
||||
inline constexpr VkResult VK_SUBOPTIMAL_KHR = 1000001003;
|
||||
inline constexpr VkResult VK_ERROR_INITIALIZATION_FAILED = -3;
|
||||
inline constexpr VkResult VK_ERROR_EXTENSION_NOT_PRESENT = -7;
|
||||
inline constexpr VkResult VK_ERROR_OUT_OF_DATE_KHR = -1000001004;
|
||||
|
||||
#endif
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_VULKAN_ABI_H_
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_VULKAN_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
#define MOCKTAIL_GRAPHICS_VULKAN_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Composites the process-local RGBA text surface into application-owned
|
||||
// swapchain images. The adapter retains ownership of every Vulkan object it
|
||||
// registers. DestroySwapchain and DestroyDevice must be called before the
|
||||
// corresponding host Vulkan destroy function.
|
||||
//
|
||||
// An unsupported device or swapchain remains a valid registration failure:
|
||||
// QueuePresent then forwards the unmodified VkPresentInfoKHR to fallback.
|
||||
class VulkanTextOverlayCompositor final {
|
||||
public:
|
||||
VulkanTextOverlayCompositor();
|
||||
~VulkanTextOverlayCompositor();
|
||||
|
||||
VulkanTextOverlayCompositor(const VulkanTextOverlayCompositor&) = delete;
|
||||
VulkanTextOverlayCompositor& operator=(const VulkanTextOverlayCompositor&) =
|
||||
delete;
|
||||
|
||||
// enabled_features describes the features actually enabled by
|
||||
// vkCreateDevice, not merely the features supported by the physical device.
|
||||
// The supplied proc-address callbacks must bypass the Android-facing Vulkan
|
||||
// adapter and resolve host functions directly.
|
||||
bool RegisterDevice(VkInstance instance, VkPhysicalDevice physical_device,
|
||||
VkDevice device, std::uint32_t api_version,
|
||||
const char* const* enabled_extensions,
|
||||
std::uint32_t enabled_extension_count,
|
||||
std::uint32_t graphics_queue_family,
|
||||
std::uint32_t graphics_queue_count,
|
||||
const VkPhysicalDeviceFeatures2* enabled_features,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
bool RegisterQueue(VkDevice device, VkQueue queue, std::uint32_t queue_family,
|
||||
std::uint32_t queue_index);
|
||||
|
||||
bool RegisterSwapchain(VkDevice device, VkSwapchainKHR swapchain,
|
||||
const VkSwapchainCreateInfoKHR& create_info,
|
||||
const VkImage* images, std::uint32_t image_count);
|
||||
void DestroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
|
||||
|
||||
// Serializes adapter-mediated submit, sparse-bind, present, and idle
|
||||
// operations on the exact queue range imported by libplacebo. Other
|
||||
// VkQueues remain independent, including the inactive-overlay fast path on
|
||||
// a non-imported queue.
|
||||
VkResult QueueSubmit(VkQueue queue, std::uint32_t submit_count,
|
||||
const VkSubmitInfo* submits, VkFence fence,
|
||||
PFN_vkQueueSubmit fallback);
|
||||
VkResult QueueSubmit2(VkQueue queue, std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits, VkFence fence,
|
||||
PFN_vkQueueSubmit2 fallback);
|
||||
VkResult QueueBindSparse(VkQueue queue, std::uint32_t bind_info_count,
|
||||
const VkBindSparseInfo* bind_info, VkFence fence,
|
||||
PFN_vkQueueBindSparse fallback);
|
||||
VkResult QueueWaitIdle(VkQueue queue, PFN_vkQueueWaitIdle fallback);
|
||||
VkResult DeviceWaitIdle(VkDevice device, PFN_vkDeviceWaitIdle fallback);
|
||||
|
||||
// Always invokes fallback when it is non-null. The original wait semaphores
|
||||
// are forwarded unchanged until the compositor successfully submits the
|
||||
// wait-collapse bridge; after that point only compositor-owned semaphores
|
||||
// are passed to fallback.
|
||||
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info,
|
||||
PFN_vkQueuePresentKHR fallback);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl* impl_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_VULKAN_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_ANDROID_LOG_OBSERVER_H_
|
||||
#define MOCKTAIL_PLATFORM_ANDROID_LOG_OBSERVER_H_
|
||||
|
||||
// Observes the fully formatted payload written through Mocktail's liblog
|
||||
// adapter. The callback must not retain tag or message; both are borrowed for
|
||||
// the duration of the call. Passing nullptr removes the current observer.
|
||||
using MocktailAndroidLogObserver = void (*)(int priority, const char* tag,
|
||||
const char* message);
|
||||
|
||||
extern "C" void mocktail_android_log_set_observer(
|
||||
MocktailAndroidLogObserver observer);
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_ANDROID_LOG_OBSERVER_H_
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_DISPLAY_REFRESH_CAPABILITIES_H_
|
||||
#define MOCKTAIL_PLATFORM_DISPLAY_REFRESH_CAPABILITIES_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
struct DisplayRefreshCapabilities {
|
||||
float current_hz = 0.0f;
|
||||
std::vector<float> supported_hz;
|
||||
|
||||
bool valid() const { return current_hz > 0.0f; }
|
||||
};
|
||||
|
||||
// Queries SDL video state. SDL requires this function to run on the thread
|
||||
// that owns the window; callers may copy the returned immutable snapshot to
|
||||
// worker-owned startup state afterwards.
|
||||
DisplayRefreshCapabilities
|
||||
QuerySdlDisplayRefreshCapabilities(SDL_Window *window);
|
||||
|
||||
// Normalizes raw display rates for deterministic publication and tests.
|
||||
DisplayRefreshCapabilities
|
||||
NormalizeDisplayRefreshCapabilities(float current_hz,
|
||||
std::vector<float> supported_hz);
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_DISPLAY_REFRESH_CAPABILITIES_H_
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
|
||||
#define MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
enum class WindowSurfaceApi {
|
||||
kAngleEgl = 0,
|
||||
kDirectVulkan,
|
||||
};
|
||||
|
||||
enum class NativeWindowSystem {
|
||||
kUnknown = 0,
|
||||
kX11,
|
||||
kWayland,
|
||||
};
|
||||
|
||||
struct WindowOptions {
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
std::string title = "Roblox";
|
||||
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
|
||||
bool high_pixel_density = true;
|
||||
bool initially_hidden = true;
|
||||
|
||||
// Production callers should keep this true. Tests using SDL's dummy driver
|
||||
// may opt out because that driver intentionally has no native WSI handle.
|
||||
bool require_native_window = true;
|
||||
};
|
||||
|
||||
// Non-owning handles captured from SDL window properties. backend_window is
|
||||
// an opaque SDL_Window pointer used only by SDL-specific graphics adapters.
|
||||
struct NativeWindowDescriptor {
|
||||
NativeWindowSystem window_system = NativeWindowSystem::kUnknown;
|
||||
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
|
||||
void* backend_window = nullptr;
|
||||
void* native_display = nullptr;
|
||||
std::uintptr_t native_window = 0;
|
||||
void* native_surface = nullptr;
|
||||
void* native_egl_window = nullptr;
|
||||
|
||||
bool HasPlatformHandle() const {
|
||||
if (window_system == NativeWindowSystem::kX11) {
|
||||
return native_display != nullptr && native_window != 0;
|
||||
}
|
||||
if (window_system == NativeWindowSystem::kWayland) {
|
||||
return native_display != nullptr && native_surface != nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct QuitEvent {};
|
||||
|
||||
struct WindowResizedEvent {
|
||||
int logical_width = 0;
|
||||
int logical_height = 0;
|
||||
int pixel_width = 0;
|
||||
int pixel_height = 0;
|
||||
};
|
||||
|
||||
struct WindowFocusEvent {
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct KeyEvent {
|
||||
bool pressed = false;
|
||||
bool repeat = false;
|
||||
std::uint32_t scancode = 0;
|
||||
std::uint32_t keycode = 0;
|
||||
std::uint32_t modifiers = 0;
|
||||
};
|
||||
|
||||
struct TextInputEvent {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct TextEditingEvent {
|
||||
std::string text;
|
||||
int start = 0;
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
struct MouseMotionEvent {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float delta_x = 0.0f;
|
||||
float delta_y = 0.0f;
|
||||
std::uint32_t buttons = 0;
|
||||
};
|
||||
|
||||
struct MouseButtonEvent {
|
||||
bool pressed = false;
|
||||
std::uint8_t button = 0;
|
||||
std::uint8_t clicks = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
};
|
||||
|
||||
struct MouseWheelEvent {
|
||||
float delta_x = 0.0f;
|
||||
float delta_y = 0.0f;
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
};
|
||||
|
||||
struct TouchEvent {
|
||||
enum class Action {
|
||||
kDown = 0,
|
||||
kMove,
|
||||
kUp,
|
||||
kCancelled,
|
||||
};
|
||||
|
||||
Action action = Action::kMove;
|
||||
std::int64_t touch_id = 0;
|
||||
std::int64_t finger_id = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float delta_x = 0.0f;
|
||||
float delta_y = 0.0f;
|
||||
float pressure = 0.0f;
|
||||
};
|
||||
|
||||
struct GamepadConnectionEvent {
|
||||
std::int64_t instance_id = 0;
|
||||
bool connected = false;
|
||||
};
|
||||
|
||||
struct GamepadAxisEvent {
|
||||
std::int64_t instance_id = 0;
|
||||
std::uint8_t axis = 0;
|
||||
std::int16_t value = 0;
|
||||
};
|
||||
|
||||
struct GamepadButtonEvent {
|
||||
std::int64_t instance_id = 0;
|
||||
std::uint8_t button = 0;
|
||||
bool pressed = false;
|
||||
};
|
||||
|
||||
using PlatformEventPayload =
|
||||
std::variant<QuitEvent, WindowResizedEvent, WindowFocusEvent, KeyEvent,
|
||||
TextInputEvent, TextEditingEvent, MouseMotionEvent,
|
||||
MouseButtonEvent, MouseWheelEvent, TouchEvent,
|
||||
GamepadConnectionEvent, GamepadAxisEvent, GamepadButtonEvent>;
|
||||
|
||||
struct PlatformEvent {
|
||||
std::uint64_t timestamp_ns = 0;
|
||||
PlatformEventPayload payload = QuitEvent{};
|
||||
};
|
||||
|
||||
// Main-thread-owned platform boundary. Implementations never create an EGL or
|
||||
// Vulkan device/context; those belong to the selected graphics backend.
|
||||
class PlatformRuntime {
|
||||
public:
|
||||
virtual ~PlatformRuntime() = default;
|
||||
|
||||
virtual Status Initialize(const WindowOptions& options) = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
virtual bool IsInitialized() const = 0;
|
||||
|
||||
// Returns OK with has_event=false when the queue currently contains no
|
||||
// supported platform event.
|
||||
virtual Status PollEvent(PlatformEvent* event, bool* has_event) = 0;
|
||||
virtual Status SetTextInputEnabled(bool enabled) = 0;
|
||||
virtual Status ShowWindow() = 0;
|
||||
|
||||
virtual NativeWindowDescriptor GetNativeWindow() const = 0;
|
||||
};
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
inline constexpr char kMocktailApplicationName[] = "Mocktail";
|
||||
inline constexpr char kMocktailApplicationIdentifier[] =
|
||||
"space.bigrat.mocktail";
|
||||
|
||||
// Configures the stable compositor identity before SDL initializes. The
|
||||
// identifier matches the installed desktop file and icon name so Wayland and
|
||||
// X11 compositors can group the runtime window with its launcher.
|
||||
Status ConfigureSdlApplicationMetadata();
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_EVENT_CONVERTER_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_EVENT_CONVERTER_H_
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
// Converts one SDL event without retaining either argument. The caller remains
|
||||
// the sole owner of SDL_PollEvent and decides how converted events are routed.
|
||||
// Returns false for invalid arguments and SDL events outside the platform
|
||||
// contract.
|
||||
bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
|
||||
PlatformEvent* destination);
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_EVENT_CONVERTER_H_
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_PLATFORM_RUNTIME_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_PLATFORM_RUNTIME_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
// Creates an SDL3-backed runtime without exposing SDL types to consumers.
|
||||
std::unique_ptr<PlatformRuntime> CreateSdlPlatformRuntime();
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_PLATFORM_RUNTIME_H_
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_H_
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
// Applies the embedded desktop artwork to an SDL window. X11 compositors use
|
||||
// this to populate _NET_WM_ICON; Wayland compositors use the desktop identity.
|
||||
Status ApplySdlWindowIcon(SDL_Window* window);
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_WINDOW_ICON_H_
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_PLATFORM_TEXT_CLIPBOARD_H_
|
||||
#define MOCKTAIL_PLATFORM_TEXT_CLIPBOARD_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
// Main-thread platform boundary for UTF-8 clipboard text. Implementations and
|
||||
// consumers must not log clipboard contents.
|
||||
class TextClipboard {
|
||||
public:
|
||||
virtual ~TextClipboard() = default;
|
||||
|
||||
virtual Status ReadText(std::string* text) = 0;
|
||||
virtual Status WriteText(const std::string& text) = 0;
|
||||
};
|
||||
|
||||
// Creates the SDL3 clipboard adapter used by both XWayland and native Wayland.
|
||||
std::unique_ptr<TextClipboard> CreateSdlTextClipboard();
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_TEXT_CLIPBOARD_H_
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
#define MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace foundation {
|
||||
|
||||
class Sha256 final {
|
||||
public:
|
||||
Sha256() = default;
|
||||
~Sha256();
|
||||
|
||||
Sha256(const Sha256&) = delete;
|
||||
Sha256& operator=(const Sha256&) = delete;
|
||||
|
||||
void Update(const unsigned char* bytes, std::size_t size);
|
||||
void Update(std::string_view bytes);
|
||||
std::string FinalHex();
|
||||
|
||||
private:
|
||||
void Transform(const unsigned char* block);
|
||||
|
||||
std::array<std::uint32_t, 8> state_ = {
|
||||
0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
|
||||
0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U,
|
||||
};
|
||||
std::array<unsigned char, 64> block_{};
|
||||
std::size_t block_bytes_ = 0;
|
||||
std::uint64_t total_bytes_ = 0;
|
||||
bool finalized_ = false;
|
||||
};
|
||||
|
||||
// Returns the lowercase SHA-256 digest of the exact byte sequence, including
|
||||
// embedded NUL bytes.
|
||||
std::string ComputeSha256Hex(std::string_view bytes);
|
||||
|
||||
} // namespace foundation
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_STATUS_H_
|
||||
#define MOCKTAIL_STATUS_H_
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
|
||||
enum class StatusCode {
|
||||
kOk = 0,
|
||||
kInvalidArgument,
|
||||
kFailedPrecondition,
|
||||
kUnavailable,
|
||||
kUnsupported,
|
||||
kPlatformError,
|
||||
};
|
||||
|
||||
class Status final {
|
||||
public:
|
||||
Status() = default;
|
||||
|
||||
static Status Ok() { return Status(); }
|
||||
|
||||
static Status Error(StatusCode code, std::string message) {
|
||||
return Status(code, std::move(message));
|
||||
}
|
||||
|
||||
bool ok() const { return code_ == StatusCode::kOk; }
|
||||
explicit operator bool() const { return ok(); }
|
||||
StatusCode code() const { return code_; }
|
||||
const std::string& message() const { return message_; }
|
||||
|
||||
private:
|
||||
Status(StatusCode code, std::string message)
|
||||
: code_(code), message_(std::move(message)) {}
|
||||
|
||||
StatusCode code_ = StatusCode::kOk;
|
||||
std::string message_;
|
||||
};
|
||||
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_STATUS_H_
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_AUTH_RUNTIME_COMPOSITION_H_
|
||||
#define MOCKTAIL_RUNTIME_AUTH_RUNTIME_COMPOSITION_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace services {
|
||||
class AuthService;
|
||||
class HttpClient;
|
||||
} // namespace services
|
||||
|
||||
namespace runtime {
|
||||
|
||||
class Environment;
|
||||
class RuntimePaths;
|
||||
|
||||
enum class AuthRuntimeStatus {
|
||||
kAuthenticated,
|
||||
kGuest,
|
||||
kInvalidCredentials,
|
||||
kUnavailable,
|
||||
};
|
||||
|
||||
// Process-local ownership for the canonical .ROBLOSECURITY header. The class
|
||||
// is move-only and clears its backing allocation before release.
|
||||
// Callers must never log the value returned by view() or c_str().
|
||||
class SecureRobloxCredential final {
|
||||
public:
|
||||
SecureRobloxCredential() = default;
|
||||
explicit SecureRobloxCredential(std::string canonical_header);
|
||||
~SecureRobloxCredential();
|
||||
|
||||
SecureRobloxCredential(const SecureRobloxCredential&) = delete;
|
||||
SecureRobloxCredential& operator=(const SecureRobloxCredential&) = delete;
|
||||
SecureRobloxCredential(SecureRobloxCredential&& other) noexcept;
|
||||
SecureRobloxCredential& operator=(SecureRobloxCredential&& other) noexcept;
|
||||
|
||||
bool empty() const { return bytes_.empty(); }
|
||||
size_t size() const { return bytes_.empty() ? 0 : bytes_.size() - 1; }
|
||||
const char* c_str() const { return bytes_.empty() ? "" : bytes_.data(); }
|
||||
std::string_view view() const { return {c_str(), size()}; }
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
std::vector<char> bytes_;
|
||||
};
|
||||
|
||||
// Clears a transient string that contained derived credential material.
|
||||
void SecurelyClearString(std::string* value);
|
||||
|
||||
// Binds the production pseudo-VM to the exact credential resolved by the
|
||||
// authentication preflight. The binding is intentionally non-movable:
|
||||
// it must be created only after its credential reaches its final address and
|
||||
// destroyed before that credential or the VM. Guest credentials block legacy
|
||||
// env/disk fallback until the sink durably accepts a native sign-in value.
|
||||
class ScopedRobloxCredentialBinding final {
|
||||
public:
|
||||
ScopedRobloxCredentialBinding(jnivm::VM* jni_vm,
|
||||
const SecureRobloxCredential& credential);
|
||||
~ScopedRobloxCredentialBinding();
|
||||
|
||||
ScopedRobloxCredentialBinding(const ScopedRobloxCredentialBinding&) = delete;
|
||||
ScopedRobloxCredentialBinding& operator=(
|
||||
const ScopedRobloxCredentialBinding&) = delete;
|
||||
ScopedRobloxCredentialBinding(ScopedRobloxCredentialBinding&&) = delete;
|
||||
ScopedRobloxCredentialBinding& operator=(
|
||||
ScopedRobloxCredentialBinding&&) = delete;
|
||||
|
||||
bool bound() const { return jni_vm_ != nullptr; }
|
||||
|
||||
private:
|
||||
static jnivm::RobloxCredentialView ProvideCredential(const void* context);
|
||||
|
||||
jnivm::VM* jni_vm_ = nullptr;
|
||||
};
|
||||
|
||||
// Result of resolving host credentials before the Android runtime starts.
|
||||
// Credential ownership is retained only so the exact credential validated by
|
||||
// AuthService can be moved into native startup without reopening its source.
|
||||
struct AuthRuntimeComposition {
|
||||
AuthRuntimeStatus status = AuthRuntimeStatus::kUnavailable;
|
||||
std::shared_ptr<jnivm::VM> jni_vm;
|
||||
jnivm::RobloxAuthIdentity account_identity;
|
||||
SecureRobloxCredential credential;
|
||||
long http_status = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return jni_vm != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the saved Roblox session and constructs the production pseudo-VM.
|
||||
// A VM is returned only for an authenticated session or when guest startup is
|
||||
// explicitly enabled with MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP. An exact HTTP 401
|
||||
// from a managed Mocktail file clears only the rejected .ROBLOSECURITY. A
|
||||
// rejected default Sober credential is suppressed by a private fingerprint
|
||||
// without modifying Sober's file. The guest policy then allows native sign-in
|
||||
// to continue. Explicit overrides, unsafe recovery, other invalid credentials,
|
||||
// and unavailable authentication still fail closed before native startup.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
services::AuthService& auth_service);
|
||||
|
||||
// Production overload retaining the HTTP transport for credentials delivered
|
||||
// by native sign-in after startup. A persisted credential is validated before
|
||||
// its account identity is promoted into the running pseudo-VM.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
services::AuthService& auth_service,
|
||||
std::shared_ptr<services::HttpClient> live_auth_http_client);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_AUTH_RUNTIME_COMPOSITION_H_
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_COMMAND_LINE_H_
|
||||
#define MOCKTAIL_RUNTIME_COMMAND_LINE_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class CommandMode {
|
||||
kRun,
|
||||
kHelp,
|
||||
};
|
||||
|
||||
enum class WindowMode {
|
||||
kUnspecified,
|
||||
kHeadless,
|
||||
kWindowed,
|
||||
};
|
||||
|
||||
struct CommandLineOptions {
|
||||
CommandMode mode = CommandMode::kRun;
|
||||
WindowMode window_mode = WindowMode::kUnspecified;
|
||||
std::string program_name = "mocktail";
|
||||
std::string roblox_library_path;
|
||||
std::string graphics_backend;
|
||||
// Raw browser/internal input exists only until the composition root creates
|
||||
// safe re-exec arguments, then it is overwritten in this string and argv.
|
||||
std::string raw_launch_argument;
|
||||
std::string launch_request_json;
|
||||
int launch_argument_index = -1;
|
||||
bool allow_unverified_build = false;
|
||||
};
|
||||
|
||||
struct CommandLineParseResult {
|
||||
CommandLineOptions options;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]);
|
||||
// Produces argv[1..] for a possible cgroup re-exec. A raw website URI is
|
||||
// replaced with the already-normalized internal request, so browser gameinfo
|
||||
// tickets do not cross the exec boundary.
|
||||
bool BuildCommandLineReexecArguments(const CommandLineOptions& options,
|
||||
int argc, const char* const argv[],
|
||||
std::vector<std::string>* arguments,
|
||||
std::string* error);
|
||||
// Overwrites the original raw launch argument and both owned launch strings
|
||||
// after the request and sanitized re-exec argv have been created.
|
||||
void ScrubCommandLineLaunchArguments(CommandLineOptions* options, int argc,
|
||||
char* argv[]);
|
||||
bool ApplyCommandLineEnvironment(const CommandLineOptions& options,
|
||||
std::string* error);
|
||||
std::string CommandLineUsage(const std::string& program_name);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_COMMAND_LINE_H_
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_CRASH_REPORT_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_CRASH_REPORT_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Applies Mocktail's mandatory privacy boundary to the flat Roblox
|
||||
// client-settings override object. The policy disables native Crashpad, Java
|
||||
// crash/ANR uploads, Lua Backtrace errors, and inferred-crash submission while
|
||||
// preserving unrelated caller overrides. Unsafe values are replaced rather
|
||||
// than treated as conflicts because crash upload is not a user-configurable
|
||||
// runtime capability.
|
||||
bool MergeCrashReportClientSettingsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies the same mandatory values to Roblox's separate AppBridge
|
||||
// fastFlagsJson channel. Only FastVariable names are inserted here;
|
||||
// Java-only client settings remain on the client-settings boundary above.
|
||||
bool MergeCrashReportFastFlagsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_CRASH_REPORT_POLICY_H_
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_DEVICE_PROFILE_H_
|
||||
#define MOCKTAIL_RUNTIME_DEVICE_PROFILE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class DeviceClass {
|
||||
kPc,
|
||||
kMobile,
|
||||
kConsole,
|
||||
};
|
||||
|
||||
// One immutable profile keeps Roblox admission, pseudo-Android hardware
|
||||
// identity, and built-in input capabilities consistent. The guest ABI and
|
||||
// client-settings group remain Android/GoogleAndroidApp.
|
||||
struct DeviceProfile {
|
||||
std::string name;
|
||||
std::string cache_key;
|
||||
DeviceClass device_class = DeviceClass::kPc;
|
||||
std::string platform_name;
|
||||
std::string display_name;
|
||||
std::string manufacturer;
|
||||
std::string model;
|
||||
std::string brand;
|
||||
std::string device_code;
|
||||
std::string device_sku;
|
||||
std::string soc_model;
|
||||
std::string roblox_http_user_agent;
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
bool keyboard_enabled = true;
|
||||
bool pc_hardware = true;
|
||||
};
|
||||
|
||||
inline constexpr std::string_view kDefaultDeviceProfileName = "pc-windows-11";
|
||||
// These identifiers select Roblox server-side admission only. They never
|
||||
// replace the Android APK ABI or its GoogleAndroidApp client-settings group.
|
||||
inline constexpr std::string_view kRobloxDesktopHttpUserAgent =
|
||||
"Roblox/WinInet";
|
||||
// The current Android-host playability endpoint recognizes this as console.
|
||||
// PlayStation-labelled user agents currently fall back to desktop admission,
|
||||
// so the PS5 marketing profile keeps this transport-only classifier.
|
||||
inline constexpr std::string_view kRobloxConsoleAdmissionUserAgent =
|
||||
"Roblox/XboxOne";
|
||||
|
||||
// Accepts canonical names and the convenient pc/mobile/console aliases.
|
||||
// Returned profiles have static lifetime.
|
||||
const DeviceProfile* FindDeviceProfile(std::string_view name);
|
||||
bool IsValidDeviceProfileValue(std::string_view value, std::size_t maximum);
|
||||
std::string BuildCustomDeviceProfileCacheKey(const DeviceProfile& profile);
|
||||
std::string_view DeviceClassName(DeviceClass device_class);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DEVICE_PROFILE_H_
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ENVIRONMENT_H_
|
||||
#define MOCKTAIL_RUNTIME_ENVIRONMENT_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Read-only environment abstraction. Production code uses ProcessEnvironment;
|
||||
// tests can provide a deterministic map-backed implementation.
|
||||
class Environment {
|
||||
public:
|
||||
virtual ~Environment() = default;
|
||||
|
||||
virtual std::optional<std::string> Get(std::string_view name) const = 0;
|
||||
|
||||
bool HasNonEmpty(std::string_view name) const;
|
||||
std::string GetOr(std::string_view name,
|
||||
std::string_view default_value) const;
|
||||
};
|
||||
|
||||
class ProcessEnvironment final : public Environment {
|
||||
public:
|
||||
std::optional<std::string> Get(std::string_view name) const override;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ENVIRONMENT_H_
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_EXTERNAL_LAUNCH_BROKER_H_
|
||||
#define MOCKTAIL_RUNTIME_EXTERNAL_LAUNCH_BROKER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
inline constexpr std::size_t kMaximumPendingExternalLaunches = 8;
|
||||
|
||||
struct ExternalLaunchBrokerOptions {
|
||||
// Empty selects the current user's protected runtime endpoint. Tests and
|
||||
// isolated launchers may provide an explicit socket under a private
|
||||
// directory owned by the current user.
|
||||
std::filesystem::path socket_path;
|
||||
std::size_t maximum_pending_launches = kMaximumPendingExternalLaunches;
|
||||
// The owner crosses configuration/bootstrap and a possible cgroup
|
||||
// re-exec before it can safely ACK. A concurrent browser click waits through
|
||||
// that bounded startup window instead of racing a not-yet-created socket.
|
||||
int forwarding_timeout_ms = 30000;
|
||||
};
|
||||
|
||||
using DispatchExternalLaunchFn =
|
||||
Status (*)(void* context, const RobloxExperienceLaunchRequest& request);
|
||||
|
||||
struct ExternalLaunchSink {
|
||||
void* context = nullptr;
|
||||
DispatchExternalLaunchFn dispatch = nullptr;
|
||||
|
||||
bool valid() const { return dispatch != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the per-user, per-installation endpoint. Owner resolution may
|
||||
// create only the private fallback directory; it never removes a stale
|
||||
// socket. Socket cleanup belongs exclusively to StartOwnerAfterLockAcquired
|
||||
// after the caller owns the matching Mocktail single-instance lock.
|
||||
Status ResolveExternalLaunchSocketPath(bool owner,
|
||||
std::filesystem::path* socket_path);
|
||||
|
||||
// Owns a bounded process-local launch queue and its same-user AF_UNIX endpoint.
|
||||
// Wire requests contain only already-normalized ExperienceProtocol JSON; the
|
||||
// raw browser URI and its gameinfo ticket never cross this boundary.
|
||||
class ExternalLaunchBroker final {
|
||||
public:
|
||||
~ExternalLaunchBroker();
|
||||
|
||||
ExternalLaunchBroker(const ExternalLaunchBroker&) = delete;
|
||||
ExternalLaunchBroker& operator=(const ExternalLaunchBroker&) = delete;
|
||||
ExternalLaunchBroker(ExternalLaunchBroker&&) = delete;
|
||||
ExternalLaunchBroker& operator=(ExternalLaunchBroker&&) = delete;
|
||||
|
||||
// The caller must hold the single-instance lock. Only this operation may
|
||||
// remove a stale socket left by a crashed owner. An optional initial request
|
||||
// is normalized and queued before the listener worker can accept later
|
||||
// browser clicks, preserving click order without a startup race.
|
||||
static Status StartOwnerAfterLockAcquired(
|
||||
ExternalLaunchBrokerOptions options,
|
||||
std::shared_ptr<ExternalLaunchBroker>* broker,
|
||||
const RobloxExperienceLaunchRequest* initial_request = nullptr);
|
||||
|
||||
// Forwards one normalized request and waits for an explicit owner ACK. The
|
||||
// operation never starts a second runtime and never logs request contents.
|
||||
static Status ForwardToOwner(const ExternalLaunchBrokerOptions& options,
|
||||
const RobloxExperienceLaunchRequest& request);
|
||||
|
||||
Status QueueInitialRequest(RobloxExperienceLaunchRequest request);
|
||||
|
||||
// Dispatches at most maximum_requests in FIFO order. A failed dispatch is
|
||||
// returned to the front of the queue, so temporary downstream backpressure
|
||||
// never drops a browser launch.
|
||||
Status Drain(const ExternalLaunchSink& sink, std::size_t maximum_requests);
|
||||
|
||||
Status Shutdown();
|
||||
std::size_t pending_launch_count() const;
|
||||
const std::filesystem::path& socket_path() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
explicit ExternalLaunchBroker(std::unique_ptr<Impl> impl);
|
||||
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// Process-global attachment used by the composition root and the dynamic
|
||||
// ExperienceProtocol runtime. shared_ptr acquisition keeps the broker alive
|
||||
// while an external request is being drained.
|
||||
Status InstallActiveExternalLaunchBroker(
|
||||
const std::shared_ptr<ExternalLaunchBroker>& broker);
|
||||
void ClearActiveExternalLaunchBroker(const ExternalLaunchBroker* broker);
|
||||
std::shared_ptr<ExternalLaunchBroker> GetActiveExternalLaunchBroker();
|
||||
Status DrainActiveExternalLaunchRequests(const ExternalLaunchSink& sink,
|
||||
std::size_t maximum_requests);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_EXTERNAL_LAUNCH_BROKER_H_
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_FAILURE_DIALOG_H_
|
||||
#define MOCKTAIL_RUNTIME_FAILURE_DIALOG_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "runtime/environment.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Returns whether an interactive launch may present a desktop failure dialog.
|
||||
// Readiness canaries and headless sessions remain strictly non-interactive.
|
||||
bool FailureDialogsEnabled(const Environment& environment);
|
||||
|
||||
// Presents one synchronous GTK failure dialog. This is intended for startup
|
||||
// failures that happen before the crash monitor can be armed.
|
||||
bool ShowFailureDialog(const Environment& environment,
|
||||
std::string_view message);
|
||||
|
||||
// Owns a small out-of-process GTK monitor. The helper is started before guest
|
||||
// threads exist and receives an explicit success disposition. If Mocktail
|
||||
// returns an error, calls _Exit(), or dies from a fatal signal, the helper
|
||||
// remains able to present the last safe user-facing message.
|
||||
class FailureDialogMonitor final {
|
||||
public:
|
||||
FailureDialogMonitor() = default;
|
||||
~FailureDialogMonitor();
|
||||
|
||||
FailureDialogMonitor(const FailureDialogMonitor&) = delete;
|
||||
FailureDialogMonitor& operator=(const FailureDialogMonitor&) = delete;
|
||||
FailureDialogMonitor(FailureDialogMonitor&& other) noexcept;
|
||||
FailureDialogMonitor& operator=(FailureDialogMonitor&& other) noexcept;
|
||||
|
||||
static FailureDialogMonitor Start(const Environment& environment,
|
||||
std::string_view initial_message);
|
||||
|
||||
bool active() const { return socket_ >= 0; }
|
||||
void SetMessage(std::string_view message);
|
||||
void MarkSuccessful();
|
||||
|
||||
private:
|
||||
FailureDialogMonitor(int socket, int helper_pid);
|
||||
void Finish(char disposition);
|
||||
|
||||
int socket_ = -1;
|
||||
int helper_pid_ = -1;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_FAILURE_DIALOG_H_
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_FRAME_RATE_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_FRAME_RATE_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
inline constexpr int kMaximumSupportedRobloxSchedulerFps = 240;
|
||||
|
||||
enum class FrameRateLimitMode {
|
||||
kDisplay,
|
||||
kFixed,
|
||||
kUnlimited,
|
||||
kInvalid,
|
||||
};
|
||||
|
||||
struct FrameRatePolicy {
|
||||
FrameRateLimitMode mode = FrameRateLimitMode::kDisplay;
|
||||
int fixed_fps = 0;
|
||||
|
||||
bool valid() const { return mode != FrameRateLimitMode::kInvalid; }
|
||||
};
|
||||
|
||||
FrameRatePolicy ParseFrameRatePolicy(std::string_view value);
|
||||
|
||||
// Merges the scheduler policy into Roblox's supported client-settings ingress.
|
||||
// Unlimited selects the unmodified payload's maximum scheduler target (240).
|
||||
// With VSync auto/off, graphics policy separately requests an unthrottled
|
||||
// Vulkan present mode when the host exposes one.
|
||||
// Existing unrelated overrides are retained. A conflicting explicit override
|
||||
// is rejected instead of silently choosing one source of truth.
|
||||
bool MergeFrameRateClientSettingsOverrides(const FrameRatePolicy &policy,
|
||||
std::string_view base_json,
|
||||
std::string *merged_json,
|
||||
std::string *error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_FRAME_RATE_POLICY_H_
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
#define MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class GameModePolicy {
|
||||
kAuto,
|
||||
kOn,
|
||||
kOff,
|
||||
};
|
||||
|
||||
bool ParseGameModePolicy(std::string_view value, GameModePolicy* policy);
|
||||
const char* GameModePolicyName(GameModePolicy policy);
|
||||
|
||||
enum class GameModeSessionState {
|
||||
kDisabled,
|
||||
kUnavailable,
|
||||
kRequestFailed,
|
||||
kAlreadyActive,
|
||||
kActive,
|
||||
kStopped,
|
||||
kStopFailed,
|
||||
};
|
||||
|
||||
const char* GameModeSessionStateName(GameModeSessionState state);
|
||||
|
||||
// libgamemode's public header exposes header-only wrappers. The shared object
|
||||
// itself exports the real_* entry points represented by this table.
|
||||
struct GameModeClientApi {
|
||||
using RequestFn = int (*)();
|
||||
using QueryFn = int (*)();
|
||||
using ErrorFn = const char* (*)();
|
||||
|
||||
RequestFn request_start = nullptr;
|
||||
RequestFn request_end = nullptr;
|
||||
QueryFn query_status = nullptr;
|
||||
ErrorFn error_string = nullptr;
|
||||
};
|
||||
|
||||
// Owns at most one ref-counted GameMode request. Missing host support and
|
||||
// daemon rejection remain nonfatal because GameMode is an optimization, not a
|
||||
// correctness dependency.
|
||||
class GameModeSession final {
|
||||
public:
|
||||
GameModeSession() = default;
|
||||
~GameModeSession();
|
||||
|
||||
GameModeSession(const GameModeSession&) = delete;
|
||||
GameModeSession& operator=(const GameModeSession&) = delete;
|
||||
GameModeSession(GameModeSession&& other) noexcept;
|
||||
GameModeSession& operator=(GameModeSession&& other) = delete;
|
||||
|
||||
static GameModeSession Start(GameModePolicy policy);
|
||||
static GameModeSession StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api);
|
||||
|
||||
GameModeSessionState state() const { return state_; }
|
||||
const std::string& detail() const { return detail_; }
|
||||
bool active() const {
|
||||
return state_ == GameModeSessionState::kActive ||
|
||||
state_ == GameModeSessionState::kAlreadyActive;
|
||||
}
|
||||
bool owns_request() const { return owns_request_; }
|
||||
|
||||
Status Stop();
|
||||
|
||||
private:
|
||||
static GameModeSession StartBound(GameModePolicy policy,
|
||||
GameModeClientApi api,
|
||||
void* library_handle);
|
||||
void CloseLibrary();
|
||||
|
||||
GameModeClientApi api_;
|
||||
void* library_handle_ = nullptr;
|
||||
GameModeSessionState state_ = GameModeSessionState::kDisabled;
|
||||
bool owns_request_ = false;
|
||||
std::string detail_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_GAME_SESSION_COORDINATOR_H_
|
||||
#define MOCKTAIL_RUNTIME_GAME_SESSION_COORDINATOR_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class GameSessionPrincipalKind {
|
||||
kLocalGuest,
|
||||
kAuthenticated,
|
||||
};
|
||||
|
||||
// Session identity carries no cookie or bearer token. The owning
|
||||
// auth service keeps secrets and exposes only a monotonically increasing
|
||||
// generation to the runtime coordinator. A local guest has no principal ID and
|
||||
// is valid only for the local placeId=0 graphics path.
|
||||
struct GameSessionPrincipal {
|
||||
GameSessionPrincipalKind kind = GameSessionPrincipalKind::kLocalGuest;
|
||||
uint64_t generation = 0;
|
||||
std::string principal_id;
|
||||
std::string base_url;
|
||||
};
|
||||
|
||||
// Opaque launch data remains owned by the join service. This keeps the state
|
||||
// machine stable while Roblox changes the exact StartGameParams schema.
|
||||
struct GameJoinRequest {
|
||||
uint64_t request_id = 0;
|
||||
int64_t place_id = 0;
|
||||
std::string launch_parameters;
|
||||
};
|
||||
|
||||
// A generation identifies one native surface lifetime. Recreated Android
|
||||
// surfaces must receive a new generation even when the host window is reused.
|
||||
struct GameSurface {
|
||||
uint64_t generation = 0;
|
||||
uintptr_t native_window = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
};
|
||||
|
||||
enum class GamePresentBackend {
|
||||
kUnknown,
|
||||
kVulkan,
|
||||
kOpenGlEs,
|
||||
};
|
||||
|
||||
// This evidence is expected to be emitted only after the graphics backend has
|
||||
// observed a successful native present operation.
|
||||
struct GamePresentEvidence {
|
||||
uint64_t surface_generation = 0;
|
||||
uint64_t frame_serial = 0;
|
||||
GamePresentBackend backend = GamePresentBackend::kUnknown;
|
||||
bool present_succeeded = false;
|
||||
};
|
||||
|
||||
// Maps ExperienceSession.onSurfaceCreated to
|
||||
// setTaskSchedulerBackgroundMode(false, "ES.onSurfaceCreated"). The adapter,
|
||||
// rather than this policy layer, owns those JNI-specific arguments.
|
||||
using SetGameForegroundFn = Status (*)(void* context);
|
||||
using StartGameFn = Status (*)(void* context,
|
||||
const GameSessionPrincipal& principal,
|
||||
const GameJoinRequest& request,
|
||||
const GameSurface& surface);
|
||||
using UpdateGameSurfaceFn = Status (*)(void* context,
|
||||
const GameSurface& surface);
|
||||
using PauseGameFn = Status (*)(void* context);
|
||||
using ResumeGameFn = Status (*)(void* context, const GameSurface& surface);
|
||||
using LeaveGameFn = Status (*)(void* context);
|
||||
using ShutdownGameRuntimeFn = Status (*)(void* context);
|
||||
|
||||
// native boundary. All callbacks are synchronous and non-throwing. The
|
||||
// coordinator serializes lifecycle invocation, matching MainGameActivity's
|
||||
// single-thread executor without embedding JNI details in the policy layer.
|
||||
struct GameSessionCapabilities {
|
||||
void* context = nullptr;
|
||||
SetGameForegroundFn set_game_foreground = nullptr;
|
||||
StartGameFn start_game = nullptr;
|
||||
UpdateGameSurfaceFn update_surface = nullptr;
|
||||
PauseGameFn pause_game = nullptr;
|
||||
ResumeGameFn resume_game = nullptr;
|
||||
LeaveGameFn leave_game = nullptr;
|
||||
ShutdownGameRuntimeFn shutdown_runtime = nullptr;
|
||||
|
||||
bool complete() const;
|
||||
};
|
||||
|
||||
struct GameSessionRequirements {
|
||||
GamePresentBackend required_present_backend = GamePresentBackend::kVulkan;
|
||||
};
|
||||
|
||||
enum class GameSessionState {
|
||||
kCreated,
|
||||
kPrincipalReady,
|
||||
kJoinRequestQueued,
|
||||
kPreparingSurface,
|
||||
kSurfaceCreated,
|
||||
kSurfaceAvailable,
|
||||
kStarting,
|
||||
kRunning,
|
||||
kUpdatingSurface,
|
||||
kPausing,
|
||||
kPaused,
|
||||
kResuming,
|
||||
kLeaving,
|
||||
kReturnedToLuaApp,
|
||||
kFailure,
|
||||
kStopping,
|
||||
kStopped,
|
||||
};
|
||||
|
||||
enum class GameSessionUpdateStatus {
|
||||
kApplied,
|
||||
kDuplicate,
|
||||
kRejected,
|
||||
};
|
||||
|
||||
struct GameSessionUpdateResult {
|
||||
GameSessionUpdateStatus status = GameSessionUpdateStatus::kRejected;
|
||||
GameSessionState state = GameSessionState::kCreated;
|
||||
Status cause = Status::Ok();
|
||||
std::string message;
|
||||
|
||||
bool ok() const { return status != GameSessionUpdateStatus::kRejected; }
|
||||
bool changed() const { return status == GameSessionUpdateStatus::kApplied; }
|
||||
};
|
||||
|
||||
struct GameSessionSnapshot {
|
||||
GameSessionState state = GameSessionState::kCreated;
|
||||
uint64_t revision = 0;
|
||||
bool principal_ready = false;
|
||||
GameSessionPrincipalKind principal_kind =
|
||||
GameSessionPrincipalKind::kLocalGuest;
|
||||
bool join_request_queued = false;
|
||||
bool surface_created = false;
|
||||
bool pending_surface_available = false;
|
||||
bool active_surface_available = false;
|
||||
bool initial_start_completed = false;
|
||||
bool game_running = false;
|
||||
bool game_paused = false;
|
||||
bool game_present_pending = false;
|
||||
bool game_presented = false;
|
||||
bool returned_to_lua_app = false;
|
||||
bool stopped = false;
|
||||
uint64_t principal_generation = 0;
|
||||
uint64_t request_id = 0;
|
||||
uint64_t prepared_surface_generation = 0;
|
||||
uint64_t pending_surface_generation = 0;
|
||||
uint64_t active_surface_generation = 0;
|
||||
uint32_t active_surface_width = 0;
|
||||
uint32_t active_surface_height = 0;
|
||||
uint64_t first_presented_frame = 0;
|
||||
std::string failure_reason;
|
||||
};
|
||||
|
||||
// Fail-closed policy for the MainGameActivity game-session branch.
|
||||
//
|
||||
// SurfaceCreated must prepare a generation before SurfaceChanged can supply its
|
||||
// native window and dimensions. QueueJoinRequest and the prepared surface are
|
||||
// independent initial prerequisites; whichever completes last starts the game
|
||||
// with the current candidate surface. Initial start never calls ResumeGame.
|
||||
// Once running, SurfaceChanged updates the active surface, SurfaceDestroyed
|
||||
// pauses, and SurfaceCreated(new generation) plus SurfaceChanged resumes.
|
||||
// Candidate surfaces become active only after the corresponding native call
|
||||
// succeeds.
|
||||
class GameSessionCoordinator final {
|
||||
public:
|
||||
explicit GameSessionCoordinator(GameSessionCapabilities capabilities,
|
||||
GameSessionRequirements requirements = {});
|
||||
|
||||
GameSessionCoordinator(const GameSessionCoordinator&) = delete;
|
||||
GameSessionCoordinator& operator=(const GameSessionCoordinator&) = delete;
|
||||
GameSessionCoordinator(GameSessionCoordinator&&) = delete;
|
||||
GameSessionCoordinator& operator=(GameSessionCoordinator&&) = delete;
|
||||
|
||||
GameSessionUpdateResult SetPrincipal(GameSessionPrincipal principal);
|
||||
GameSessionUpdateResult QueueJoinRequest(GameJoinRequest request);
|
||||
GameSessionUpdateResult SurfaceCreated(uint64_t surface_generation);
|
||||
GameSessionUpdateResult SurfaceChanged(GameSurface surface);
|
||||
// Replays the native surface update even when the handle and extent are
|
||||
// unchanged. Fullscreen compositors can invalidate a Vulkan swapchain after
|
||||
// their final state transition without changing the last reported extent.
|
||||
GameSessionUpdateResult RebindSurface(GameSurface surface);
|
||||
GameSessionUpdateResult SurfaceDestroyed(uint64_t surface_generation);
|
||||
GameSessionUpdateResult RecordGamePresent(GamePresentEvidence evidence);
|
||||
|
||||
// Leaves the current experience and returns to LuaApp. It keeps
|
||||
// the app runtime alive so composition may start another session later.
|
||||
GameSessionUpdateResult LeaveGame();
|
||||
|
||||
// Terminates the entire app runtime. It is not an alias for LeaveGame and
|
||||
// never injects an implicit leave callback before app-level teardown.
|
||||
GameSessionUpdateResult Shutdown();
|
||||
|
||||
GameSessionSnapshot Snapshot() const;
|
||||
|
||||
private:
|
||||
GameSessionUpdateResult StartIfReadyLocked(std::unique_lock<std::mutex>* lock,
|
||||
const char* accepted_message);
|
||||
GameSessionUpdateResult UpdateSurface(GameSurface surface, bool force_rebind);
|
||||
GameSessionUpdateResult ApplyLocked(GameSessionState state,
|
||||
std::string message);
|
||||
GameSessionUpdateResult DuplicateLocked(std::string message) const;
|
||||
GameSessionUpdateResult FailLocked(Status cause, std::string message);
|
||||
GameSessionUpdateResult RejectLocked(Status cause, std::string message) const;
|
||||
GameSessionUpdateResult RejectTerminalLocked(std::string operation) const;
|
||||
GameSessionSnapshot SnapshotLocked() const;
|
||||
GameSessionState PreStartStateLocked() const;
|
||||
bool IsForwardTerminalLocked() const;
|
||||
bool IsPresentShapeValidLocked(const GamePresentEvidence& evidence) const;
|
||||
void PromotePendingSurfaceLocked();
|
||||
void DiscardPendingSurfaceLocked();
|
||||
void CommitPendingPresentLocked();
|
||||
void DiscardPendingPresentLocked();
|
||||
|
||||
static bool SamePrincipal(const GameSessionPrincipal& left,
|
||||
const GameSessionPrincipal& right);
|
||||
static bool SameRequest(const GameJoinRequest& left,
|
||||
const GameJoinRequest& right);
|
||||
static bool SameSurface(const GameSurface& left, const GameSurface& right);
|
||||
|
||||
// Lifecycle methods take operation_mutex_ before mutex_. Native callbacks
|
||||
// run without mutex_, so Vulkan evidence can be recorded synchronously. A
|
||||
// callback must not re-enter another lifecycle method; render evidence is the
|
||||
// only supported synchronous re-entry.
|
||||
mutable std::mutex operation_mutex_;
|
||||
mutable std::mutex mutex_;
|
||||
const GameSessionCapabilities capabilities_;
|
||||
const GameSessionRequirements requirements_;
|
||||
|
||||
GameSessionState state_ = GameSessionState::kCreated;
|
||||
uint64_t revision_ = 0;
|
||||
bool principal_ready_ = false;
|
||||
bool join_request_queued_ = false;
|
||||
bool surface_created_ = false;
|
||||
bool pending_surface_available_ = false;
|
||||
bool active_surface_available_ = false;
|
||||
bool initial_start_completed_ = false;
|
||||
bool game_paused_ = false;
|
||||
bool pending_present_ = false;
|
||||
bool game_presented_ = false;
|
||||
bool returned_to_lua_app_ = false;
|
||||
bool shutdown_attempted_ = false;
|
||||
uint64_t prepared_surface_generation_ = 0;
|
||||
uint64_t last_destroyed_surface_generation_ = 0;
|
||||
GameSessionPrincipal principal_;
|
||||
GameJoinRequest request_;
|
||||
GameSurface pending_surface_;
|
||||
GameSurface active_surface_;
|
||||
GamePresentEvidence pending_present_evidence_;
|
||||
uint64_t first_presented_frame_ = 0;
|
||||
std::string failure_reason_;
|
||||
};
|
||||
|
||||
const char* GameSessionStateName(GameSessionState state);
|
||||
const char* GamePresentBackendName(GamePresentBackend backend);
|
||||
const char* GameSessionPrincipalKindName(GameSessionPrincipalKind kind);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_GAME_SESSION_COORDINATOR_H_
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_GRAPHICS_LAUNCH_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_GRAPHICS_LAUNCH_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Publishes the resolved graphics backend before the managed payload updater
|
||||
// starts. OpenGL is a strict system EGL/GLES path; it never silently retries
|
||||
// through ANGLE/Vulkan or accepts a window without a real graphics context.
|
||||
bool ApplyGraphicsLaunchPolicy(const RuntimeConfig& config,
|
||||
std::string* error = nullptr);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_GRAPHICS_LAUNCH_POLICY_H_
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_HTTP_CLIENT_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_HTTP_CLIENT_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Applies the supported Android HttpClient compatibility mode. The policy
|
||||
// keeps endpoint-specific retries intact while disabling the generic LuaApp
|
||||
// retry source and Roblox's RuntimeMutexRv backend, whose busy retry loop and
|
||||
// lock traffic are disproportionately expensive across the Bionic bridge.
|
||||
bool MergeHttpClientSettingsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_HTTP_CLIENT_POLICY_H_
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_JNIVM_PLATFORM_WEB_CALLBACKS_H_
|
||||
#define MOCKTAIL_RUNTIME_JNIVM_PLATFORM_WEB_CALLBACKS_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// adapter kept outside the research runtime. The opaque context must be
|
||||
// the production jnivm::VM owned by the startup composition.
|
||||
bool SetJnivmPlatformWebCallbacks(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*on_data_model_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_app_bridge_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_native_overlay)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_open_web_activity)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_sync_cookies)(void*, JNIEnv*, jstring),
|
||||
void (*on_set_cookie)(void*, JNIEnv*, jstring, jstring));
|
||||
void ClearJnivmPlatformWebCallbacks(void* context);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_JNIVM_PLATFORM_WEB_CALLBACKS_H_
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_MEMORY_LIMIT_H_
|
||||
#define MOCKTAIL_RUNTIME_MEMORY_LIMIT_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/owned_pthread.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct ProcessMemoryUsage {
|
||||
std::uint64_t resident_bytes = 0;
|
||||
std::uint64_t swap_bytes = 0;
|
||||
bool valid = false;
|
||||
|
||||
std::uint64_t committed_bytes() const {
|
||||
return resident_bytes + swap_bytes;
|
||||
}
|
||||
};
|
||||
|
||||
// Parses Linux /proc/<pid>/status memory fields. VmRSS is required; VmSwap is
|
||||
// optional because some kernels omit it when swap support is disabled.
|
||||
ProcessMemoryUsage ParseProcessStatusMemory(std::string_view status);
|
||||
|
||||
enum class CgroupMemoryLimitStatus {
|
||||
kActive,
|
||||
kUnavailable,
|
||||
kExecFailed,
|
||||
};
|
||||
|
||||
struct CgroupMemoryLimitResult {
|
||||
CgroupMemoryLimitStatus status = CgroupMemoryLimitStatus::kUnavailable;
|
||||
std::string detail;
|
||||
|
||||
bool active() const { return status == CgroupMemoryLimitStatus::kActive; }
|
||||
};
|
||||
|
||||
// Replaces the current process with the same command inside a transient
|
||||
// systemd user scope when cgroup v2 delegation is available. A successful
|
||||
// re-exec does not return. Systems without systemd continue with the watchdog
|
||||
// fallback and receive kUnavailable.
|
||||
CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
||||
int argc, char* const argv[], std::uint64_t limit_bytes,
|
||||
const std::vector<std::string>* reexec_arguments = nullptr);
|
||||
|
||||
// Portable fallback for Linux systems where a delegated cgroup cannot be
|
||||
// created. It watches the current process's actual RSS plus swap and exits with
|
||||
// status 137 before a runaway guest can keep exhausting host memory.
|
||||
class MemoryLimitWatchdog final {
|
||||
public:
|
||||
MemoryLimitWatchdog() = default;
|
||||
~MemoryLimitWatchdog();
|
||||
|
||||
MemoryLimitWatchdog(const MemoryLimitWatchdog&) = delete;
|
||||
MemoryLimitWatchdog& operator=(const MemoryLimitWatchdog&) = delete;
|
||||
|
||||
bool Start(std::uint64_t limit_bytes, std::string* error);
|
||||
void Stop();
|
||||
|
||||
bool running() const { return running_; }
|
||||
|
||||
private:
|
||||
static void* ThreadEntry(void* context);
|
||||
void Run();
|
||||
|
||||
std::atomic<bool> stop_requested_{false};
|
||||
std::uint64_t limit_bytes_ = 0;
|
||||
OwnedPthread worker_;
|
||||
bool running_ = false;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_MEMORY_LIMIT_H_
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_OWNED_PTHREAD_H_
|
||||
#define MOCKTAIL_RUNTIME_OWNED_PTHREAD_H_
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
using PthreadEntryPoint = void* (*)(void* context);
|
||||
using PthreadWaitPump = void (*)(void* context);
|
||||
|
||||
enum class OwnedPthreadWaitStatus {
|
||||
kJoined,
|
||||
kTimedOut,
|
||||
kPlatformError,
|
||||
};
|
||||
|
||||
struct OwnedPthreadWaitResult {
|
||||
OwnedPthreadWaitStatus status = OwnedPthreadWaitStatus::kPlatformError;
|
||||
int platform_error = 0;
|
||||
|
||||
bool joined() const { return status == OwnedPthreadWaitStatus::kJoined; }
|
||||
};
|
||||
|
||||
struct OwnedPthreadCancelResult {
|
||||
int cancel_error = 0;
|
||||
OwnedPthreadWaitResult wait;
|
||||
};
|
||||
|
||||
// Owns a joinable Linux pthread. There is intentionally no detach operation:
|
||||
// context and every dependency reachable by a worker must outlive a physical
|
||||
// join. Destroying a still-joinable instance terminates the process without
|
||||
// unwinding, preventing a live worker from observing released stack or RAII
|
||||
// state.
|
||||
class OwnedPthread final {
|
||||
public:
|
||||
OwnedPthread() = default;
|
||||
~OwnedPthread();
|
||||
|
||||
OwnedPthread(const OwnedPthread&) = delete;
|
||||
OwnedPthread& operator=(const OwnedPthread&) = delete;
|
||||
|
||||
int Start(PthreadEntryPoint entry_point, void* context,
|
||||
std::size_t stack_size);
|
||||
OwnedPthreadWaitResult WaitFor(int timeout_ms, int poll_interval_ms,
|
||||
PthreadWaitPump pump = nullptr,
|
||||
void* pump_context = nullptr);
|
||||
OwnedPthreadCancelResult CancelAndJoinFor(int timeout_ms,
|
||||
int poll_interval_ms,
|
||||
PthreadWaitPump pump = nullptr,
|
||||
void* pump_context = nullptr);
|
||||
int Signal(int signal_number) const;
|
||||
|
||||
bool joinable() const { return joinable_; }
|
||||
|
||||
private:
|
||||
pthread_t thread_{};
|
||||
bool joinable_ = false;
|
||||
};
|
||||
|
||||
const char* OwnedPthreadWaitStatusName(OwnedPthreadWaitStatus status);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_OWNED_PTHREAD_H_
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_PAYLOAD_UPDATE_PREFLIGHT_H_
|
||||
#define MOCKTAIL_RUNTIME_PAYLOAD_UPDATE_PREFLIGHT_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct PayloadUpdatePreflightResult {
|
||||
bool attempted = false;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
const Environment& environment, const RuntimePaths& paths);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PAYLOAD_UPDATE_PREFLIGHT_H_
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "runtime/frame_rate_policy.h"
|
||||
#include "runtime/game_mode.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class PhysicsWorkerMode {
|
||||
kAuto,
|
||||
kLatency,
|
||||
kThroughput,
|
||||
};
|
||||
|
||||
bool ParsePhysicsWorkerMode(std::string_view value, PhysicsWorkerMode* mode);
|
||||
std::string_view PhysicsWorkerModeName(PhysicsWorkerMode mode);
|
||||
|
||||
// Makes every available physical core eligible for Roblox scheduler work.
|
||||
// At least one scheduler worker remains on malformed topology input.
|
||||
int CalculateThroughputWorkerCount(int available_physical_cores);
|
||||
|
||||
struct PerformancePolicy {
|
||||
bool multithreaded_rendering = false;
|
||||
int physical_core_count = 0;
|
||||
std::uint64_t memory_limit_mb = 0;
|
||||
bool memory_limit_valid = true;
|
||||
GameModePolicy game_mode = GameModePolicy::kAuto;
|
||||
bool game_mode_valid = true;
|
||||
PhysicsWorkerMode physics_worker_mode = PhysicsWorkerMode::kAuto;
|
||||
bool physics_worker_mode_valid = true;
|
||||
|
||||
bool memory_limit_enabled() const {
|
||||
return memory_limit_valid && memory_limit_mb != 0;
|
||||
}
|
||||
std::uint64_t memory_limit_bytes() const {
|
||||
return memory_limit_mb * 1024U * 1024U;
|
||||
}
|
||||
};
|
||||
|
||||
PerformancePolicy ParsePerformancePolicy(
|
||||
std::string_view multithreaded_rendering,
|
||||
std::string_view memory_limit_mb = "0", std::string_view game_mode = "auto",
|
||||
std::string_view physics_worker_mode = "auto");
|
||||
|
||||
// Counts unique physical cores available to the current process. Linux CPU
|
||||
// affinity is respected; systems without readable topology fall back to the
|
||||
// available logical CPU count.
|
||||
int DetectAvailablePhysicalCoreCount();
|
||||
|
||||
// Merges the supported Roblox scheduler/rendering mode into the flat
|
||||
// client-settings override object. Disabled policy leaves existing overrides
|
||||
// intact. Conflicting explicit values fail closed instead of silently
|
||||
// replacing the caller's settings.
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies every runtime policy in a stable order before the resulting
|
||||
// override object crosses the legacy startup boundary. The mandatory
|
||||
// HttpClient compatibility policy is applied before the mandatory crash-report
|
||||
// upload block. Neither policy can be overridden by a caller-provided client
|
||||
// setting.
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_PLATFORM_CACHE_MIGRATION_H_
|
||||
#define MOCKTAIL_RUNTIME_PLATFORM_CACHE_MIGRATION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
class Environment;
|
||||
class RuntimePaths;
|
||||
|
||||
// Every coordinate affects Roblox's server-refreshed platform policy. Keeping
|
||||
// the canonical preset and input capabilities in one cache fingerprint
|
||||
// prevents one emulated device from reusing another device's hydration data.
|
||||
std::string BuildPlatformProfileRevision(std::string_view device_profile,
|
||||
bool touch_enabled, bool mouse_enabled,
|
||||
bool keyboard_enabled);
|
||||
|
||||
struct PlatformCacheMigrationResult {
|
||||
bool transitioned = false;
|
||||
bool app_storage_found = false;
|
||||
bool app_storage_updated = false;
|
||||
std::filesystem::path app_storage_file;
|
||||
std::filesystem::path fingerprint_file;
|
||||
std::string previous_revision;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Invalidates only platform-derived, server-refreshable appStorage entries
|
||||
// when the platform profile revision changes. Installation identity,
|
||||
// OTA state, credentials, and unrelated preferences remain untouched.
|
||||
PlatformCacheMigrationResult MigratePlatformProfileCaches(
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
std::string_view desired_revision);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PLATFORM_CACHE_MIGRATION_H_
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_APP_LIFECYCLE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_APP_LIFECYCLE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/roblox_capability_resolver.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
using NativeAppBridgeV2PauseAppFn = void (*)(JNIEnv *env, jclass clazz);
|
||||
using SetTaskSchedulerBackgroundModeFn = void (*)(JNIEnv *env, jclass clazz,
|
||||
jboolean enabled,
|
||||
jstring context);
|
||||
using NativeAppBridgeV2DestroyAppFn = void (*)(JNIEnv *env, jclass clazz);
|
||||
|
||||
struct RobloxAppLifecycleSymbols {
|
||||
NativeAppBridgeV2PauseAppFn app_bridge_v2_pause_app = nullptr;
|
||||
SetTaskSchedulerBackgroundModeFn set_task_scheduler_background_mode = nullptr;
|
||||
NativeAppBridgeV2DestroyAppFn app_bridge_v2_destroy_app = nullptr;
|
||||
|
||||
bool complete() const;
|
||||
};
|
||||
|
||||
enum class RobloxAppLifecycleResolutionStatus {
|
||||
kReady,
|
||||
kInvalidLookup,
|
||||
kMissingRequiredSymbols,
|
||||
};
|
||||
|
||||
class RobloxAppLifecycleResolution final {
|
||||
public:
|
||||
RobloxAppLifecycleResolutionStatus status() const { return status_; }
|
||||
bool ok() const {
|
||||
return status_ == RobloxAppLifecycleResolutionStatus::kReady;
|
||||
}
|
||||
const RobloxAppLifecycleSymbols *symbols() const {
|
||||
return ok() ? &symbols_ : nullptr;
|
||||
}
|
||||
const std::vector<std::string> &missing_required_symbols() const {
|
||||
return missing_required_symbols_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend RobloxAppLifecycleResolution
|
||||
ResolveRobloxAppLifecycleSymbols(const RobloxSymbolLookup &lookup);
|
||||
|
||||
RobloxAppLifecycleResolutionStatus status_ =
|
||||
RobloxAppLifecycleResolutionStatus::kInvalidLookup;
|
||||
RobloxAppLifecycleSymbols symbols_;
|
||||
std::vector<std::string> missing_required_symbols_;
|
||||
};
|
||||
|
||||
RobloxAppLifecycleResolution
|
||||
ResolveRobloxAppLifecycleSymbols(const RobloxSymbolLookup &lookup);
|
||||
|
||||
enum class RobloxAppShutdownStatus {
|
||||
kStopped,
|
||||
kAlreadyStopped,
|
||||
kShutdownInProgress,
|
||||
kMissingRequiredSymbols,
|
||||
kInvalidJniState,
|
||||
kReasonAllocationFailed,
|
||||
};
|
||||
|
||||
struct RobloxAppShutdownResult {
|
||||
RobloxAppShutdownStatus status =
|
||||
RobloxAppShutdownStatus::kMissingRequiredSymbols;
|
||||
const char *message = "lifecycle symbols are incomplete";
|
||||
|
||||
bool ok() const {
|
||||
return status == RobloxAppShutdownStatus::kStopped ||
|
||||
status == RobloxAppShutdownStatus::kAlreadyStopped;
|
||||
}
|
||||
};
|
||||
|
||||
// Owns only lifecycle state and function pointers. The Android library
|
||||
// handle and WSI resources remain owned by composition, ensuring
|
||||
// they stay mapped/alive throughout native teardown.
|
||||
class RobloxAppLifecycle final {
|
||||
public:
|
||||
explicit RobloxAppLifecycle(RobloxAppLifecycleSymbols symbols);
|
||||
|
||||
RobloxAppLifecycle(const RobloxAppLifecycle &) = delete;
|
||||
RobloxAppLifecycle &operator=(const RobloxAppLifecycle &) = delete;
|
||||
|
||||
// Runs the supported ASMA shutdown sequence exactly once:
|
||||
// pause app -> scheduler background(true, "ASMA.stop") -> destroy app.
|
||||
RobloxAppShutdownResult Shutdown(JNIEnv *env, jclass native_gl_class);
|
||||
|
||||
bool stopped() const;
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
kReady,
|
||||
kStopping,
|
||||
kStopped,
|
||||
};
|
||||
|
||||
RobloxAppLifecycleSymbols symbols_;
|
||||
std::atomic<State> state_{State::kReady};
|
||||
};
|
||||
|
||||
const char *RobloxAppLifecycleResolutionStatusName(
|
||||
RobloxAppLifecycleResolutionStatus status);
|
||||
const char *RobloxAppShutdownStatusName(RobloxAppShutdownStatus status);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_APP_LIFECYCLE_H_
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_BROWSER_SERVICE_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_BROWSER_SERVICE_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
#include "runtime/webview_helper_launcher.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
inline constexpr char kRobloxBrowserServiceOpenWindowKey[] =
|
||||
"BrowserService.OpenBrowserWindow";
|
||||
inline constexpr char kRobloxBrowserServiceCloseWindowKey[] =
|
||||
"BrowserService.CloseBrowserWindow";
|
||||
inline constexpr char kRobloxBrowserServiceSendCommandKey[] =
|
||||
"BrowserService.SendCommand";
|
||||
inline constexpr char kRobloxBrowserServiceExecuteJavaScriptKey[] =
|
||||
"BrowserService.ExecuteJavaScript";
|
||||
inline constexpr char kRobloxBrowserServiceJavaScriptCallbackKey[] =
|
||||
"BrowserService.JavaScriptCallback";
|
||||
inline constexpr char kRobloxBrowserServiceBrowserWindowClosedKey[] =
|
||||
"BrowserService.BrowserWindowClosed";
|
||||
inline constexpr std::size_t kMaximumRobloxBrowserServicePayloadBytes =
|
||||
64 * 1024;
|
||||
inline constexpr std::size_t kMaximumRobloxBrowserServiceUrlBytes = 16 * 1024;
|
||||
inline constexpr std::size_t kMaximumRobloxBrowserServiceQueuedEvents = 64;
|
||||
inline constexpr std::size_t kMaximumRobloxBrowserServiceQueuedBytes =
|
||||
1024 * 1024;
|
||||
|
||||
// Owned browser request copied before Callback.onItemSet(String) returns.
|
||||
// URL policy and navigation remain the responsibility of the downstream
|
||||
// launcher; this boundary validates only the APK contract and bounded input.
|
||||
// Raw OpenBrowserWindow leaves every presentation property absent, while the
|
||||
// SendCommand open form engages each property with the APK's default value.
|
||||
struct RobloxBrowserServiceOpenRequest {
|
||||
std::string url;
|
||||
std::optional<std::string> title;
|
||||
std::optional<bool> visible;
|
||||
std::optional<bool> modal;
|
||||
std::optional<bool> back_navigation_disabled;
|
||||
std::optional<bool> show_domain_as_title;
|
||||
};
|
||||
|
||||
// BrowserService.SendCommand's config form preserves field absence. The APK
|
||||
// changes only properties present in the JSON object.
|
||||
struct RobloxBrowserServiceConfigRequest {
|
||||
std::optional<std::string> title;
|
||||
std::optional<bool> visible;
|
||||
std::optional<bool> back_navigation_disabled;
|
||||
};
|
||||
|
||||
struct RobloxBrowserServiceExecuteRequest {
|
||||
std::string source;
|
||||
};
|
||||
|
||||
using RobloxBrowserServiceCommand =
|
||||
std::variant<RobloxBrowserServiceOpenRequest,
|
||||
RobloxBrowserServiceConfigRequest>;
|
||||
|
||||
Status ParseRobloxBrowserServiceOpenWindow(
|
||||
const std::string& payload, RobloxBrowserServiceOpenRequest* request);
|
||||
Status ParseRobloxBrowserServiceSendCommand(
|
||||
const std::string& json, RobloxBrowserServiceCommand* command);
|
||||
|
||||
using DispatchRobloxBrowserServiceOpenFn =
|
||||
Status (*)(void* context, const RobloxBrowserServiceOpenRequest& request,
|
||||
WebViewHelperExitObserver exit_observer);
|
||||
using DispatchRobloxBrowserServiceConfigFn =
|
||||
Status (*)(void* context, const RobloxBrowserServiceConfigRequest& request);
|
||||
using DispatchRobloxBrowserServiceCloseFn = Status (*)(void *context);
|
||||
using DispatchRobloxBrowserServiceExecuteFn = Status (*)(
|
||||
void* context, const RobloxBrowserServiceExecuteRequest& request);
|
||||
|
||||
struct RobloxBrowserServiceSink {
|
||||
void *context = nullptr;
|
||||
DispatchRobloxBrowserServiceOpenFn dispatch_open = nullptr;
|
||||
DispatchRobloxBrowserServiceCloseFn dispatch_close = nullptr;
|
||||
DispatchRobloxBrowserServiceConfigFn dispatch_config = nullptr;
|
||||
DispatchRobloxBrowserServiceExecuteFn dispatch_execute = nullptr;
|
||||
|
||||
bool valid() const {
|
||||
return dispatch_open != nullptr && dispatch_close != nullptr &&
|
||||
dispatch_config != nullptr && dispatch_execute != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
using BindRobloxMemStorageFn = jobject (*)(JNIEnv *, jclass, jstring, jobject);
|
||||
using DisconnectRobloxMemStorageFn = void (*)(JNIEnv *, jclass, jobject);
|
||||
using ReleaseRobloxMemStorageConnectionFn = void (*)(JNIEnv *, jclass, jobject);
|
||||
using FireRobloxMemStorageFn = void (*)(JNIEnv*, jclass, jstring, jstring);
|
||||
|
||||
struct RobloxBrowserServiceSymbols {
|
||||
BindRobloxMemStorageFn bind = nullptr;
|
||||
DisconnectRobloxMemStorageFn disconnect = nullptr;
|
||||
ReleaseRobloxMemStorageConnectionFn release_connection = nullptr;
|
||||
FireRobloxMemStorageFn fire = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return bind != nullptr && disconnect != nullptr &&
|
||||
release_connection != nullptr && fire != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxBrowserServiceCallbackFactory {
|
||||
void *context = nullptr;
|
||||
jobject (*create_callback)(void *context,
|
||||
std::shared_ptr<void> callback_context,
|
||||
void (*on_item_set)(void *, JNIEnv *,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_callback)(void *context, jobject callback) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return context != nullptr && create_callback != nullptr &&
|
||||
clear_callback != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// equivalent of the current APK's yg.c and gi.a BrowserService bridges.
|
||||
// It binds the four exact MemStorage keys and owns every Callback and
|
||||
// Connection until a disconnect-and-release shutdown has completed. Outgoing
|
||||
// WebView events are queued without JNI and must be drained by the platform
|
||||
// main thread.
|
||||
class RobloxBrowserServiceBridge final {
|
||||
public:
|
||||
RobloxBrowserServiceBridge(JniEnvironmentProvider environment,
|
||||
RobloxBrowserServiceSymbols symbols,
|
||||
RobloxBrowserServiceCallbackFactory factory,
|
||||
RobloxBrowserServiceSink sink);
|
||||
~RobloxBrowserServiceBridge();
|
||||
|
||||
RobloxBrowserServiceBridge(const RobloxBrowserServiceBridge &) = delete;
|
||||
RobloxBrowserServiceBridge& operator=(const RobloxBrowserServiceBridge&) =
|
||||
delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
Status HandleOwnedOpenWindow(std::string payload);
|
||||
Status HandleOwnedSendCommand(std::string payload);
|
||||
Status HandleOwnedExecuteJavaScript(std::string payload);
|
||||
Status HandleCloseWindow();
|
||||
|
||||
Status QueueJavaScriptCallback(std::string payload);
|
||||
Status QueueBrowserWindowClosed();
|
||||
Status DrainOutgoingEvents();
|
||||
|
||||
bool initialized() const;
|
||||
|
||||
private:
|
||||
enum class EventKind {
|
||||
kOpenWindow,
|
||||
kCloseWindow,
|
||||
kSendCommand,
|
||||
kExecuteJavaScript,
|
||||
};
|
||||
|
||||
enum class OutgoingEventKind {
|
||||
kJavaScriptCallback,
|
||||
kBrowserWindowClosed,
|
||||
};
|
||||
|
||||
struct OutgoingEvent {
|
||||
OutgoingEventKind kind = OutgoingEventKind::kJavaScriptCallback;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
struct CallbackTarget;
|
||||
struct BrowserWindowCloseTarget;
|
||||
struct BrowserWindowExitContext;
|
||||
|
||||
Status CheckJniException(JNIEnv *env, const char *operation) const;
|
||||
Status BeginDispatch();
|
||||
void EndDispatch();
|
||||
Status DispatchOpenRequest(RobloxBrowserServiceOpenRequest request);
|
||||
Status QueueOutgoingEvent(OutgoingEventKind kind, std::string payload);
|
||||
void FinishOutgoingDrain(std::deque<OutgoingEvent> remaining_events);
|
||||
Status HandleItemSet(EventKind kind, JNIEnv *env, jstring payload);
|
||||
static void ItemSetCallback(void *context, JNIEnv *env, jstring payload);
|
||||
static void BrowserWindowExited(void* context);
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxBrowserServiceSymbols symbols_;
|
||||
const RobloxBrowserServiceCallbackFactory factory_;
|
||||
const RobloxBrowserServiceSink sink_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable dispatch_drained_;
|
||||
jclass connection_class_ = nullptr;
|
||||
std::array<jobject, 4> callbacks_{};
|
||||
std::array<jobject, 4> connections_{};
|
||||
std::array<std::shared_ptr<CallbackTarget>, 4> callback_targets_{};
|
||||
std::shared_ptr<BrowserWindowCloseTarget> browser_window_close_target_;
|
||||
std::deque<OutgoingEvent> outgoing_events_;
|
||||
std::size_t outgoing_event_bytes_ = 0;
|
||||
std::size_t outgoing_drain_events_ = 0;
|
||||
std::size_t outgoing_drain_bytes_ = 0;
|
||||
std::size_t in_flight_dispatches_ = 0;
|
||||
bool outgoing_drain_active_ = false;
|
||||
bool initializing_ = false;
|
||||
bool shutting_down_ = false;
|
||||
bool initialized_ = false;
|
||||
bool accepting_messages_ = false;
|
||||
std::atomic<uint64_t> next_browser_window_generation_{1};
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_BROWSER_SERVICE_BRIDGE_H_
|
||||
@@ -0,0 +1,261 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_CAPABILITY_RESOLVER_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_CAPABILITY_RESOLVER_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Non-owning callback used to look up exported symbols. The resolver invokes
|
||||
// callbacks synchronously and never retains the callback or its context.
|
||||
class RobloxSymbolLookup {
|
||||
public:
|
||||
using Callback = void* (*)(void* context, const char* symbol_name);
|
||||
|
||||
RobloxSymbolLookup() = default;
|
||||
RobloxSymbolLookup(Callback callback, void* context)
|
||||
: callback_(callback), context_(context) {}
|
||||
|
||||
bool valid() const { return callback_ != nullptr; }
|
||||
void* Find(const char* symbol_name) const;
|
||||
|
||||
private:
|
||||
Callback callback_ = nullptr;
|
||||
void* context_ = nullptr;
|
||||
};
|
||||
|
||||
struct RobloxCapabilityRequirements {
|
||||
// A real windowed startup also requires the app-surface update entrypoint.
|
||||
bool windowed = false;
|
||||
};
|
||||
|
||||
using JniOnLoadFn = jint (*)(JavaVM* vm, void* reserved);
|
||||
using NativeGameGlobalInitFn = void (*)(JNIEnv* env, jclass clazz);
|
||||
using NativeUpdateAdapterInitFn = void (*)(JNIEnv* env, jclass clazz);
|
||||
using NativeAppBridgeV2InitWithParamsFn = void (*)(JNIEnv* env, jclass clazz,
|
||||
jobject init_params);
|
||||
using NativeAppBridgeStartLuaAppDmFn = void (*)(JNIEnv* env, jclass clazz);
|
||||
using NativeAppBridgeV2StartAppWithParamsFn =
|
||||
void (*)(JNIEnv* env, jclass clazz, jobject start_app_params);
|
||||
using NativeAppBridgeV2UpdateSurfaceAppFn = void (*)(JNIEnv* env, jclass clazz,
|
||||
jobject surface,
|
||||
jobject platform_params);
|
||||
using NativeCallMessagesFromMainThreadFn = void (*)(JNIEnv* env, jclass clazz);
|
||||
using NativeAppBridgeV2StartGameWithParamFn =
|
||||
jint (*)(JNIEnv* env, jclass clazz, jobject start_game_params);
|
||||
using NativeAppBridgeV2UpdateSurfaceGameFn = void (*)(JNIEnv* env, jclass clazz,
|
||||
jobject surface,
|
||||
jobject platform_params,
|
||||
jobject activity);
|
||||
|
||||
struct RobloxStartupSymbols {
|
||||
JniOnLoadFn jni_on_load = nullptr;
|
||||
NativeGameGlobalInitFn game_global_init = nullptr;
|
||||
NativeUpdateAdapterInitFn update_adapter_init = nullptr;
|
||||
NativeAppBridgeV2InitWithParamsFn app_bridge_v2_init_with_params = nullptr;
|
||||
NativeAppBridgeStartLuaAppDmFn app_bridge_start_lua_app_dm = nullptr;
|
||||
NativeAppBridgeV2StartAppWithParamsFn app_bridge_v2_start_app_with_params =
|
||||
nullptr;
|
||||
NativeAppBridgeV2UpdateSurfaceAppFn
|
||||
app_bridge_v2_update_surface_app_with_platform_params = nullptr;
|
||||
|
||||
// Optional post-startup capabilities. Callers must use the corresponding
|
||||
// flags instead of assuming that a particular Roblox build exports them.
|
||||
NativeCallMessagesFromMainThreadFn call_messages_from_main_thread = nullptr;
|
||||
NativeAppBridgeV2StartGameWithParamFn app_bridge_v2_start_game_with_param =
|
||||
nullptr;
|
||||
NativeAppBridgeV2UpdateSurfaceGameFn
|
||||
app_bridge_v2_update_surface_game_with_platform_params = nullptr;
|
||||
};
|
||||
|
||||
struct RobloxStartupCapabilityFlags {
|
||||
bool has_surface_app = false;
|
||||
bool has_message_pump = false;
|
||||
bool has_start_game = false;
|
||||
bool has_surface_game = false;
|
||||
};
|
||||
|
||||
using NativeGamepadAxisEventFn = void (*)(JNIEnv*, jclass, jint, jint, jfloat,
|
||||
jfloat, jfloat);
|
||||
using NativeGamepadButtonEventFn = void (*)(JNIEnv*, jclass, jint, jint, jint);
|
||||
using NativeGamepadConnectEventWithGamepadTypeFn = void (*)(JNIEnv*, jclass,
|
||||
jint, jint);
|
||||
using NativeGamepadDisconnectEventFn = void (*)(JNIEnv*, jclass, jint);
|
||||
using NativeGetMainWindowIsMouseLockedCenterFn = jboolean (*)(JNIEnv*, jclass);
|
||||
using NativePassVector3Fn = void (*)(JNIEnv*, jclass, jfloat, jfloat, jfloat);
|
||||
using NativePassGyroscopeChangeFn = void (*)(JNIEnv*, jclass, jfloat, jfloat,
|
||||
jfloat, jfloat, jfloat, jfloat,
|
||||
jfloat);
|
||||
using NativePassInputFn = void (*)(JNIEnv*, jclass, jint, jfloat, jfloat, jint,
|
||||
jint, jint);
|
||||
using NativePassLongPressGestureFn = void (*)(JNIEnv*, jclass, jint, jfloat,
|
||||
jfloat);
|
||||
using NativePassMouseButtonFn = void (*)(JNIEnv*, jclass, jfloat, jfloat,
|
||||
jboolean, jint);
|
||||
using NativePassMouseVector4Fn = void (*)(JNIEnv*, jclass, jfloat, jfloat,
|
||||
jfloat, jfloat);
|
||||
using NativePassMouseVector3Fn = void (*)(JNIEnv*, jclass, jfloat, jfloat,
|
||||
jfloat);
|
||||
using NativePassKeyEventFn = void (*)(JNIEnv*, jclass, jboolean, jint, jint,
|
||||
jboolean);
|
||||
using NativeSyncTextboxTextAndCursorPosition2Fn = void (*)(JNIEnv*, jclass,
|
||||
jstring, jint);
|
||||
using NativeGetTextBoxInfoFn = jobject (*)(JNIEnv*, jclass);
|
||||
using NativePassTextFn = void (*)(JNIEnv*, jclass, jlong, jstring, jboolean,
|
||||
jint);
|
||||
using NativeTextboxHandleFn = void (*)(JNIEnv*, jclass, jlong);
|
||||
using NativePassPanGestureMultitouchFn = void (*)(JNIEnv*, jclass, jint,
|
||||
jfloatArray, jfloat, jfloat,
|
||||
jfloat, jfloat);
|
||||
using NativePassGestureWithVelocityFn = void (*)(JNIEnv*, jclass, jint, jfloat,
|
||||
jfloat, jfloat, jfloat, jfloat,
|
||||
jfloat);
|
||||
using NativePassTapGestureFn = void (*)(JNIEnv*, jclass, jfloat, jfloat);
|
||||
using NativeSetSensorEnabledFn = void (*)(JNIEnv*, jclass, jboolean);
|
||||
using NativeSetGamepadSupportedKeyWithGamepadTypeFn = void (*)(JNIEnv*, jclass,
|
||||
jint, jint,
|
||||
jboolean, jint);
|
||||
using NativeSetGamepadSupportedMotionWithGamepadTypeFn =
|
||||
void (*)(JNIEnv*, jclass, jint, jint, jint, jboolean, jint);
|
||||
using NativeSetTvRemoteSupportedKeyFn = void (*)(JNIEnv*, jclass, jint, jint,
|
||||
jboolean);
|
||||
using NativeTvRemoteButtonEventFn = void (*)(JNIEnv*, jclass, jint, jint, jint);
|
||||
using NativeTvRemoteConnectEventFn = void (*)(JNIEnv*, jclass, jint);
|
||||
using NativeUpdateScreenOrientationFn = void (*)(JNIEnv*, jclass, jint);
|
||||
|
||||
struct RobloxInputSymbols {
|
||||
NativeGamepadAxisEventFn gamepad_axis_event = nullptr;
|
||||
NativeGamepadButtonEventFn gamepad_button_event = nullptr;
|
||||
NativeGamepadConnectEventWithGamepadTypeFn
|
||||
gamepad_connect_event_with_gamepad_type = nullptr;
|
||||
NativeGamepadDisconnectEventFn gamepad_disconnect_event = nullptr;
|
||||
NativeGetMainWindowIsMouseLockedCenterFn
|
||||
get_main_window_is_mouse_locked_center = nullptr;
|
||||
NativePassVector3Fn pass_accelerometer_change = nullptr;
|
||||
NativePassVector3Fn pass_gravity_change = nullptr;
|
||||
NativePassGyroscopeChangeFn pass_gyroscope_change = nullptr;
|
||||
NativePassInputFn pass_input = nullptr;
|
||||
NativePassLongPressGestureFn pass_long_press_gesture = nullptr;
|
||||
NativePassMouseButtonFn pass_mouse_button = nullptr;
|
||||
NativePassMouseVector4Fn pass_mouse_move = nullptr;
|
||||
NativePassMouseVector4Fn pass_mouse_pan = nullptr;
|
||||
NativePassMouseVector3Fn pass_mouse_pinch = nullptr;
|
||||
NativePassMouseVector3Fn pass_mouse_wheel = nullptr;
|
||||
NativePassKeyEventFn pass_key_event = nullptr;
|
||||
NativeSyncTextboxTextAndCursorPosition2Fn
|
||||
sync_textbox_text_and_cursor_position2 = nullptr;
|
||||
NativeGetTextBoxInfoFn get_text_box_info = nullptr;
|
||||
NativePassTextFn pass_text = nullptr;
|
||||
NativeTextboxHandleFn return_pressed_from_on_screen_keyboard = nullptr;
|
||||
NativeTextboxHandleFn release_focus = nullptr;
|
||||
NativePassPanGestureMultitouchFn pass_pan_gesture_multitouch = nullptr;
|
||||
NativePassGestureWithVelocityFn pass_pan_gesture_with_velocity = nullptr;
|
||||
NativePassGestureWithVelocityFn pass_pinch_gesture = nullptr;
|
||||
NativePassGestureWithVelocityFn pass_rotate_gesture = nullptr;
|
||||
NativePassLongPressGestureFn pass_swipe_gesture = nullptr;
|
||||
NativePassTapGestureFn pass_tap_gesture = nullptr;
|
||||
NativeSetSensorEnabledFn set_accelerometer_enabled = nullptr;
|
||||
NativeSetGamepadSupportedKeyWithGamepadTypeFn
|
||||
set_gamepad_supported_key_with_gamepad_type = nullptr;
|
||||
NativeSetGamepadSupportedMotionWithGamepadTypeFn
|
||||
set_gamepad_supported_motion_with_gamepad_type = nullptr;
|
||||
NativeSetSensorEnabledFn set_gyroscope_enabled = nullptr;
|
||||
NativeSetTvRemoteSupportedKeyFn set_tv_remote_supported_key = nullptr;
|
||||
NativeTvRemoteButtonEventFn tv_remote_button_event = nullptr;
|
||||
NativeTvRemoteConnectEventFn tv_remote_connect_event = nullptr;
|
||||
NativeTvRemoteConnectEventFn tv_remote_disconnect_event = nullptr;
|
||||
NativeUpdateScreenOrientationFn update_screen_orientation = nullptr;
|
||||
};
|
||||
|
||||
enum class RobloxGamepadApi {
|
||||
kUnsupported,
|
||||
kWithGamepadType,
|
||||
};
|
||||
|
||||
struct RobloxInputCapabilityFlags {
|
||||
RobloxGamepadApi gamepad_api = RobloxGamepadApi::kUnsupported;
|
||||
bool has_gamepad_events = false;
|
||||
bool has_mouse = false;
|
||||
bool has_touch = false;
|
||||
bool has_hardware_keyboard = false;
|
||||
bool has_text_input = false;
|
||||
bool has_gestures = false;
|
||||
bool has_sensors = false;
|
||||
bool has_tv_remote = false;
|
||||
bool has_screen_orientation = false;
|
||||
|
||||
// unsupported: these exports were removed from current Roblox
|
||||
// builds and must not be selected as a fallback for the modern API.
|
||||
bool has_legacy_gamepad_api = false;
|
||||
bool has_ptf_gamepad_api = false;
|
||||
};
|
||||
|
||||
struct RobloxCapabilities {
|
||||
RobloxStartupSymbols startup;
|
||||
RobloxStartupCapabilityFlags startup_flags;
|
||||
RobloxInputSymbols input;
|
||||
RobloxInputCapabilityFlags input_flags;
|
||||
};
|
||||
|
||||
enum class RobloxCapabilityResolutionStatus {
|
||||
kReady,
|
||||
kInvalidLookup,
|
||||
kMissingRequiredSymbols,
|
||||
};
|
||||
|
||||
// A failed resolution withholds the partially resolved function
|
||||
// table. This prevents callers from starting Roblox after a required export was
|
||||
// not found, while still exposing exact missing symbol names for diagnostics.
|
||||
class RobloxCapabilityResolution {
|
||||
public:
|
||||
RobloxCapabilityResolutionStatus status() const { return status_; }
|
||||
bool ok() const {
|
||||
return status_ == RobloxCapabilityResolutionStatus::kReady;
|
||||
}
|
||||
const RobloxCapabilities* capabilities() const {
|
||||
return ok() ? &capabilities_ : nullptr;
|
||||
}
|
||||
const std::vector<std::string>& missing_required_symbols() const {
|
||||
return missing_required_symbols_;
|
||||
}
|
||||
|
||||
private:
|
||||
friend RobloxCapabilityResolution ResolveRobloxCapabilities(
|
||||
const RobloxSymbolLookup& lookup,
|
||||
RobloxCapabilityRequirements requirements);
|
||||
|
||||
RobloxCapabilityResolutionStatus status_ =
|
||||
RobloxCapabilityResolutionStatus::kInvalidLookup;
|
||||
RobloxCapabilities capabilities_;
|
||||
std::vector<std::string> missing_required_symbols_;
|
||||
};
|
||||
|
||||
RobloxCapabilityResolution ResolveRobloxCapabilities(
|
||||
const RobloxSymbolLookup& lookup,
|
||||
RobloxCapabilityRequirements requirements = {});
|
||||
|
||||
const char* RobloxCapabilityResolutionStatusName(
|
||||
RobloxCapabilityResolutionStatus status);
|
||||
const char* RobloxGamepadApiName(RobloxGamepadApi api);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_CAPABILITY_RESOLVER_H_
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_DESKTOP_APP_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_DESKTOP_APP_POLICY_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DesktopAppPolicyResult {
|
||||
bool app_storage_created = false;
|
||||
bool updated = false;
|
||||
std::size_t normalized_policy_count = 0;
|
||||
std::string policy_json;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Normalizes only Roblox UniversalApp layout coordinates for a PC profile.
|
||||
// Account eligibility and feature-entitlement fields remain server-owned.
|
||||
// The guest and authenticated cache keys are both populated so user-switch
|
||||
// startup cannot fall back to the Android Unknown/mobile layout.
|
||||
DesktopAppPolicyResult ApplyDesktopAppPolicy(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
const std::filesystem::path& default_policy_file,
|
||||
std::int64_t authenticated_user_id);
|
||||
|
||||
// Routes the normalized policy through Roblox's supported runtime override so
|
||||
// a later GUAC response cannot replace the in-memory desktop layout with the
|
||||
// Android Unknown layout after startup.
|
||||
bool MergeDesktopAppPolicyClientSettingsOverride(std::string_view policy_json,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_DESKTOP_APP_POLICY_H_
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_COMPOSITION_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_COMPOSITION_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_fresh_game_launch_controller.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
#include "runtime/webview_roblox_cookie.h"
|
||||
|
||||
namespace jnivm {
|
||||
class VM;
|
||||
} // namespace jnivm
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
class RobloxExperienceCompositionWebSurfaceTest;
|
||||
|
||||
// Pseudo-JVM integration needed to materialize the APK RawCallback interface.
|
||||
// The composition obtains MessageBus.f() itself through its exact Java API.
|
||||
struct RobloxExperienceJniFactory {
|
||||
void* context = nullptr;
|
||||
jobject (*create_raw_callback)(void* context,
|
||||
std::shared_ptr<void> callback_context,
|
||||
void (*run)(void*, JNIEnv*,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_raw_callback)(void* context, jobject callback) = nullptr;
|
||||
jobject (*create_request_handler)(void* context,
|
||||
std::shared_ptr<void> callback_context,
|
||||
std::string (*run)(void*, JNIEnv*,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_request_handler)(void* context, jobject handler) = nullptr;
|
||||
jobject (*create_mem_storage_callback)(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*on_item_set)(void*, JNIEnv*, jstring)) = nullptr;
|
||||
void (*clear_mem_storage_callback)(void* context, jobject callback) = nullptr;
|
||||
bool (*set_platform_web_callbacks)(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*on_data_model_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_app_bridge_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_native_overlay)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_open_web_activity)(void*, JNIEnv*, jstring, jstring),
|
||||
void (*on_sync_cookies)(void*, JNIEnv*, jstring),
|
||||
void (*on_set_cookie)(void*, JNIEnv*, jstring, jstring)) = nullptr;
|
||||
void (*clear_platform_web_callbacks)(void* context) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return context != nullptr && create_raw_callback != nullptr &&
|
||||
clear_raw_callback != nullptr && create_request_handler != nullptr &&
|
||||
clear_request_handler != nullptr &&
|
||||
create_mem_storage_callback != nullptr &&
|
||||
clear_mem_storage_callback != nullptr &&
|
||||
set_platform_web_callbacks != nullptr &&
|
||||
clear_platform_web_callbacks != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Immutable values captured when LuaApp becomes ready. No Stage 6 local JNI
|
||||
// reference is accepted here: the owner builds a fresh object graph and keeps
|
||||
// its roots as global references until process-level shutdown.
|
||||
struct RobloxLuaAppExperienceReadiness {
|
||||
GameSessionPrincipal principal;
|
||||
GameSurface surface;
|
||||
std::string username;
|
||||
bool is_under_13 = false;
|
||||
int32_t join_request_type = -1;
|
||||
|
||||
bool complete() const;
|
||||
};
|
||||
|
||||
// Supplies the current host surface to a composition that becomes
|
||||
// authenticated after LuaApp startup. Production defaults to the SDL window
|
||||
// snapshot; tests may inject an equivalent source.
|
||||
struct RobloxExperienceSurfaceProvider {
|
||||
void* context = nullptr;
|
||||
GameSurface (*snapshot)(void* context) = nullptr;
|
||||
|
||||
bool valid() const { return snapshot != nullptr; }
|
||||
};
|
||||
|
||||
// Production composition for the APK platform protocols and ExperienceProtocol
|
||||
// paths. InitializePlatformProtocols retains MessageBus.f(), installs the
|
||||
// WebViewProtocol, and binds the durable BrowserService MemStorage observers
|
||||
// before native bootstrap. OnLuaAppReady then adds the game JNI roots and
|
||||
// ExperienceProtocol subscription. Every owned JSON launch request is
|
||||
// dispatched to a fresh UGCGame controller without environment-derived fields.
|
||||
class RobloxExperienceComposition final {
|
||||
public:
|
||||
RobloxExperienceComposition(
|
||||
JniEnvironmentProvider environment,
|
||||
RobloxExperienceMessageBusSymbols message_bus_symbols,
|
||||
RobloxWebViewMessageBusSymbols web_view_symbols,
|
||||
RobloxBrowserServiceSymbols browser_service_symbols,
|
||||
RobloxGameSessionSymbols game_symbols,
|
||||
RobloxExperienceJniFactory jni_factory,
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
RobloxGameSurfaceJniConfig surface_config = {},
|
||||
const SecureRobloxCredential* initial_web_view_credential = nullptr,
|
||||
RobloxExperienceSurfaceProvider surface_provider = {});
|
||||
~RobloxExperienceComposition();
|
||||
|
||||
RobloxExperienceComposition(const RobloxExperienceComposition&) = delete;
|
||||
RobloxExperienceComposition& operator=(const RobloxExperienceComposition&) =
|
||||
delete;
|
||||
|
||||
Status InitializePlatformProtocols();
|
||||
Status OnLuaAppReady(RobloxLuaAppExperienceReadiness readiness);
|
||||
// Callback-safe notification from the native app lifecycle. The callback
|
||||
// never invokes JNI or waits for an in-flight launch worker.
|
||||
void NotifyLuaAppDidReturn();
|
||||
Status DrainPlatformEvents();
|
||||
Status DrainLaunchRequests();
|
||||
Status LeaveGame();
|
||||
GameSessionUpdateResult SurfaceCreated(uint64_t generation);
|
||||
GameSessionUpdateResult SurfaceChanged(GameSurface surface);
|
||||
GameSessionUpdateResult SurfaceDestroyed(uint64_t generation);
|
||||
Status Shutdown();
|
||||
|
||||
bool subscribed() const;
|
||||
GameSessionSnapshot Snapshot() const;
|
||||
|
||||
private:
|
||||
friend class RobloxExperienceCompositionWebSurfaceTest;
|
||||
struct GlobalObjects;
|
||||
struct LaunchTask;
|
||||
struct WebSurfaceExitTarget;
|
||||
struct WebSurfaceExitContext;
|
||||
struct LifecycleTarget;
|
||||
|
||||
enum class WebSurfaceRoute {
|
||||
kNone,
|
||||
kWebView,
|
||||
kBrowserService,
|
||||
};
|
||||
|
||||
struct WebSurfacePresentation {
|
||||
std::string title = "Roblox";
|
||||
bool visible = true;
|
||||
bool back_navigation_disabled = false;
|
||||
bool show_domain_as_title = false;
|
||||
};
|
||||
|
||||
static Status DispatchLaunch(void* context,
|
||||
const RobloxExperienceLaunchRequest& request);
|
||||
Status DrainExternalLaunchRequests();
|
||||
static Status DispatchWebViewOpen(void* context,
|
||||
const RobloxWebViewOpenRequest& request,
|
||||
WebViewHelperExitObserver exit_observer);
|
||||
static Status DispatchWebViewMutate(
|
||||
void* context, const RobloxWebViewMutationRequest& request);
|
||||
static Status DispatchWebViewClose(void* context);
|
||||
static Status DispatchWebViewCookie(void* context,
|
||||
std::string_view canonical_header);
|
||||
static Status DispatchBrowserServiceOpen(
|
||||
void* context, const RobloxBrowserServiceOpenRequest& request,
|
||||
WebViewHelperExitObserver exit_observer);
|
||||
static Status DispatchBrowserServiceClose(void* context);
|
||||
static Status DispatchBrowserServiceConfig(
|
||||
void* context, const RobloxBrowserServiceConfigRequest& request);
|
||||
static Status DispatchBrowserServiceExecute(
|
||||
void* context, const RobloxBrowserServiceExecuteRequest& request);
|
||||
static void WebSurfaceExited(void* context);
|
||||
static void NotifyLateLuaAppDidReturn(void* context);
|
||||
static void* RunLaunchWorker(void* context);
|
||||
static Status RouteWebSurfaceEvent(WebSurfaceRoute route,
|
||||
const WebViewHelperEvent& event,
|
||||
RobloxWebViewBridge* web_view_bridge);
|
||||
Status RouteCurrentWebSurfaceEvent(
|
||||
const std::shared_ptr<WebViewHelperProcess>& source_process,
|
||||
uint64_t process_generation, uint64_t logical_generation,
|
||||
const WebViewHelperEvent& event);
|
||||
Status OpenWebSurface(const std::string& url, const char* transport,
|
||||
WebSurfaceRoute route,
|
||||
WebViewHelperExitObserver exit_observer,
|
||||
const WebSurfacePresentation& presentation);
|
||||
// WebViewProtocol and BrowserService are two logical transports over the
|
||||
// APK's single physical WebView container. Either protocol may close or
|
||||
// mutate the surface that the other one opened.
|
||||
Status CloseWebSurface();
|
||||
void HandleWebSurfaceExit(uint64_t process_generation);
|
||||
Status Dispatch(const RobloxExperienceLaunchRequest& request);
|
||||
Status PromoteAuthenticatedSession();
|
||||
Status RefreshLateSurface();
|
||||
void RunActiveLaunch();
|
||||
Status BuildPlatformGlobalObjects();
|
||||
Status BuildLuaAppGlobalObjects();
|
||||
Status BuildLaunchObjects(const GameSurface& surface, jobject* surface_object,
|
||||
jobject* platform_params);
|
||||
Status BuildLuaAppStartParams(
|
||||
JNIEnv* env, const RobloxLuaAppExperienceReadiness& readiness,
|
||||
jobject surface_object, jobject platform_params, jobject activity,
|
||||
jobject* start_app_params);
|
||||
Status RebindLuaAppSurface(const GameSurface& surface);
|
||||
Status RestartLuaAppSurface(const GameSurface& surface);
|
||||
Status ReleaseLuaAppGlobalObjects();
|
||||
Status ReleaseGlobalObjects();
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxExperienceMessageBusSymbols message_bus_symbols_;
|
||||
const RobloxWebViewMessageBusSymbols web_view_symbols_;
|
||||
const RobloxBrowserServiceSymbols browser_service_symbols_;
|
||||
const RobloxGameSessionSymbols game_symbols_;
|
||||
const RobloxExperienceJniFactory jni_factory_;
|
||||
const RobloxFreshLaunchPresentBoundary present_boundary_;
|
||||
const RobloxGameSurfaceJniConfig surface_config_;
|
||||
const bool consumes_window_surface_events_;
|
||||
const RobloxExperienceSurfaceProvider surface_provider_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::mutex surface_operation_mutex_;
|
||||
std::mutex web_surface_operation_mutex_;
|
||||
RobloxLuaAppExperienceReadiness readiness_;
|
||||
std::unique_ptr<GlobalObjects> objects_;
|
||||
std::shared_ptr<RobloxFreshGameLaunchController> controller_;
|
||||
std::unique_ptr<RobloxExperienceLaunchBridge> bridge_;
|
||||
std::unique_ptr<RobloxWebViewBridge> web_view_bridge_;
|
||||
std::unique_ptr<RobloxBrowserServiceBridge> browser_service_bridge_;
|
||||
std::shared_ptr<WebViewHelperProcess> web_surface_process_;
|
||||
std::shared_ptr<WebSurfaceExitTarget> web_surface_exit_target_;
|
||||
std::shared_ptr<LifecycleTarget> lifecycle_target_;
|
||||
WebViewHelperExitObserver web_surface_logical_exit_observer_;
|
||||
WebSurfaceRoute web_surface_route_ = WebSurfaceRoute::kNone;
|
||||
uint64_t web_surface_process_generation_ = 0;
|
||||
uint64_t web_surface_logical_generation_ = 0;
|
||||
uint64_t next_web_surface_process_generation_ = 1;
|
||||
uint64_t next_web_surface_logical_generation_ = 1;
|
||||
SecureWebViewRobloxCookie web_view_cookie_;
|
||||
std::string web_view_cookie_initialization_error_;
|
||||
bool web_view_cookie_synchronized_ = false;
|
||||
std::deque<RobloxExperienceLaunchRequest> pending_launch_requests_;
|
||||
std::unique_ptr<LaunchTask> active_launch_;
|
||||
std::string active_game_canonical_json_;
|
||||
OwnedPthread launch_worker_;
|
||||
uint64_t next_request_id_ = 1;
|
||||
bool platform_protocols_initialized_ = false;
|
||||
bool subscribed_ = false;
|
||||
bool launch_in_progress_ = false;
|
||||
bool game_active_ = false;
|
||||
bool lua_app_return_pending_ = false;
|
||||
bool controlled_switch_waiting_for_return_ = false;
|
||||
bool lua_app_surface_recreation_pending_ = false;
|
||||
bool late_surface_tracking_ = false;
|
||||
GameSurface late_surface_snapshot_;
|
||||
jnivm::VM* late_lifecycle_vm_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_COMPOSITION_H_
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_LAUNCH_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_LAUNCH_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
inline constexpr std::size_t kMaximumExperienceLaunchJsonBytes = 64 * 1024;
|
||||
|
||||
// Owned equivalent of the APK ug.l launch contract. Strings are copied before
|
||||
// the MessageBus RawCallback returns, so no JNI local reference escapes.
|
||||
struct RobloxExperienceLaunchRequest {
|
||||
int64_t place_id = 0;
|
||||
int64_t user_id = 0;
|
||||
int64_t conversation_id = 0;
|
||||
int64_t referred_by_player_id = 0;
|
||||
std::string game_instance_id;
|
||||
std::string reserved_server_access_code;
|
||||
std::string call_id;
|
||||
std::string referral_page;
|
||||
std::string access_code;
|
||||
std::string link_code;
|
||||
std::string launch_data;
|
||||
std::string event_id;
|
||||
std::string game_join_context;
|
||||
std::string join_attempt_id;
|
||||
std::string join_attempt_origin;
|
||||
std::string iso_context;
|
||||
std::string canonical_json;
|
||||
};
|
||||
|
||||
Status ParseRobloxExperienceLaunchJson(const std::string &json,
|
||||
RobloxExperienceLaunchRequest *request);
|
||||
|
||||
GameJoinRequest
|
||||
MakeGameJoinRequest(uint64_t request_id,
|
||||
const RobloxExperienceLaunchRequest &request);
|
||||
|
||||
using DispatchExperienceLaunchFn =
|
||||
Status (*)(void *context, const RobloxExperienceLaunchRequest &request);
|
||||
|
||||
struct RobloxExperienceLaunchSink {
|
||||
void *context = nullptr;
|
||||
DispatchExperienceLaunchFn dispatch = nullptr;
|
||||
|
||||
bool valid() const { return dispatch != nullptr; }
|
||||
};
|
||||
|
||||
using GetExperienceLaunchIdFn = jstring (*)(JNIEnv *, jclass);
|
||||
using SubscribeExperienceLaunchRawFn = jobject (*)(JNIEnv *, jobject, jstring,
|
||||
jobject, jboolean);
|
||||
using DeleteMessageBusConnectionFn = void (*)(JNIEnv *, jobject, jlong);
|
||||
|
||||
struct RobloxExperienceMessageBusSymbols {
|
||||
GetExperienceLaunchIdFn get_launch_id = nullptr;
|
||||
SubscribeExperienceLaunchRawFn subscribe_raw = nullptr;
|
||||
DeleteMessageBusConnectionFn delete_connection = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return get_launch_id != nullptr && subscribe_raw != nullptr &&
|
||||
delete_connection != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxExperienceMessageBusObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void *callback_factory_context = nullptr;
|
||||
jobject (*create_raw_callback)(void *factory_context,
|
||||
std::shared_ptr<void> callback_context,
|
||||
void (*run)(void *, JNIEnv *,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_raw_callback)(void *factory_context, jobject callback) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus != nullptr && callback_factory_context != nullptr &&
|
||||
create_raw_callback != nullptr && clear_raw_callback != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// API-first adapter for JNIExperienceProtocol + MessageBus.doSubscribeRaw.
|
||||
//
|
||||
// The pseudo-JVM must route RawCallback.run(String) to HandleRawMessage. It
|
||||
// must also preserve Connection.<init>(J)'s f10205a field; Initialize rejects a
|
||||
// connection whose native shared_ptr handle cannot be recovered, because such
|
||||
// a subscription cannot be released safely.
|
||||
class RobloxExperienceLaunchBridge final {
|
||||
public:
|
||||
RobloxExperienceLaunchBridge(JniEnvironmentProvider environment,
|
||||
RobloxExperienceMessageBusSymbols symbols,
|
||||
RobloxExperienceMessageBusObjects objects,
|
||||
RobloxExperienceLaunchSink sink);
|
||||
~RobloxExperienceLaunchBridge();
|
||||
|
||||
RobloxExperienceLaunchBridge(const RobloxExperienceLaunchBridge &) = delete;
|
||||
RobloxExperienceLaunchBridge &
|
||||
operator=(const RobloxExperienceLaunchBridge &) = delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
// Entry point for the pseudo-JVM RawCallback.run(String) shim.
|
||||
Status HandleRawMessage(JNIEnv *env, jstring message);
|
||||
|
||||
// Owned entry point used by tests and by alternative upstream MessageBus
|
||||
// adapters. It follows the same bounded parser and dispatch path.
|
||||
Status HandleOwnedMessage(std::string message);
|
||||
|
||||
bool initialized() const;
|
||||
|
||||
private:
|
||||
Status CheckJniException(JNIEnv *env, const char *operation) const;
|
||||
Status BeginDispatch();
|
||||
void EndDispatch();
|
||||
static void RawMessageCallback(void *context, JNIEnv *env, jstring message);
|
||||
|
||||
struct RawCallbackTarget;
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxExperienceMessageBusSymbols symbols_;
|
||||
const RobloxExperienceMessageBusObjects source_objects_;
|
||||
const RobloxExperienceLaunchSink sink_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable dispatch_drained_;
|
||||
jobject message_bus_ = nullptr;
|
||||
jobject raw_callback_ = nullptr;
|
||||
jobject connection_ = nullptr;
|
||||
jlong connection_handle_ = 0;
|
||||
std::shared_ptr<RawCallbackTarget> raw_callback_target_;
|
||||
std::size_t in_flight_dispatches_ = 0;
|
||||
bool initializing_ = false;
|
||||
bool shutting_down_ = false;
|
||||
bool initialized_ = false;
|
||||
bool accepting_messages_ = false;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_LAUNCH_BRIDGE_H_
|
||||
Loaded 100 of 4279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user