Migrated from GitLab

This commit is contained in:
komaruworld committed 2026-08-12 01:58:18 +03:00
commit 4e78c7d8b3
4279 files changed
+2025790

No files matched your search

+3
View File
@@ -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
+155
View File
@@ -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
+255
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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()
+202
View File
@@ -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.
+165
View File
@@ -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}'
+106
View File
@@ -0,0 +1,106 @@
# Mocktail
[![CI](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml/badge.svg)](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
[![Stars](https://img.shields.io/github/stars/komaruworld/mocktail?style=flat&logo=github)](https://github.com/komaruworld/mocktail/stargazers)
[![Downloads](https://img.shields.io/github/downloads/komaruworld/mocktail/total?logo=github)](https://github.com/komaruworld/mocktail/releases/tag/continuous)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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.
+208
View File
@@ -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()
+47
View File
@@ -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()
+100
View File
@@ -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()
+159
View File
@@ -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()
+34
View File
@@ -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_
+93
View File
@@ -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
+16
View File
@@ -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"
}
]
}
+47
View File
@@ -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."
}
]
}
+97
View File
@@ -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"
}
}
}
+23
View File
@@ -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"
}
+8
View File
@@ -0,0 +1,8 @@
{
"schema_version": 1,
"package": "com.roblox.client",
"trusted_sha256": [
"2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a",
"44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477"
]
}
+15
View File
@@ -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
+12
View File
@@ -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
+84
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+97
View File
@@ -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_
+49
View File
@@ -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_
+68
View File
@@ -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_
+123
View File
@@ -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_
+33
View File
@@ -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_
+32
View File
@@ -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_
+49
View File
@@ -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_
+86
View File
@@ -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_
+45
View File
@@ -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_
+69
View File
@@ -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_
+32
View File
@@ -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_
+93
View File
@@ -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_
+136
View File
@@ -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_
+84
View File
@@ -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_
+34
View File
@@ -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_
+45
View File
@@ -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_
+44
View File
@@ -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_
+508
View File
@@ -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_
+95
View File
@@ -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_
+126
View File
@@ -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_
+165
View File
@@ -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_
+85
View File
@@ -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_
+102
View File
@@ -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_
+432
View File
@@ -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_
+88
View File
@@ -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_
+40
View File
@@ -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_
+76
View File
@@ -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_
+59
View File
@@ -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_
+57
View File
@@ -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_
+133
View File
@@ -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_
+75
View File
@@ -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_
+44
View File
@@ -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_
+74
View File
@@ -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_
+46
View File
@@ -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_
+116
View File
@@ -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_
+66
View File
@@ -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_
+56
View File
@@ -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_
+103
View File
@@ -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_
+280
View File
@@ -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_
+34
View File
@@ -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_
+35
View File
@@ -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_
+93
View File
@@ -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_
+82
View File
@@ -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_
+92
View File
@@ -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_
+129
View File
@@ -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