mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 20:00:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
169e65bf60 | ||
|
|
69476f0f47 | ||
|
|
79d0cdfb40 | ||
|
|
273209b9ea | ||
|
|
bbfd603e17 | ||
|
|
1ba2941e7f | ||
|
|
68c90ce428 | ||
|
|
068f07e125 | ||
|
|
a469689bbb | ||
|
|
51836a980d | ||
|
|
ad546a7781 | ||
|
|
3c45450dfc | ||
|
|
185c11b57f | ||
|
|
2c3d7b485e | ||
|
|
6f8344a28b | ||
|
|
b60c43e8f0 | ||
|
|
b5e4273277 | ||
|
|
d2858fedc8 | ||
|
|
326a0659ea | ||
|
|
dd45467056 | ||
|
|
94b1036296 | ||
|
|
4c61daad0c | ||
|
|
f5f224875a | ||
|
|
90d4891a4e | ||
|
|
46dd99266c | ||
|
|
77fce18a95 | ||
|
|
d6f0f2c755 | ||
|
|
c6afc5af24 | ||
|
|
74e7455ee1 | ||
|
|
000d021452 | ||
|
|
2eef2c3821 | ||
|
|
4928443f90 | ||
|
|
b37497db7b | ||
|
|
6bbdc39d7e | ||
|
|
1c11336ba1 | ||
|
|
b19bd8406f | ||
|
|
cd46538eb8 | ||
|
|
eca5f395eb | ||
|
|
26767e925c | ||
|
|
01a0e40463 | ||
|
|
a11e833e06 | ||
|
|
b9bce36824 | ||
|
|
14c8a55211 | ||
|
|
5c5422fa02 | ||
|
|
9b041ec2ac | ||
|
|
5f83c4a64f | ||
|
|
a9d8e3185e | ||
|
|
67a9f4db97 | ||
|
|
f07daccc82 | ||
|
|
2eef4f966f | ||
|
|
41003efb09 | ||
|
|
29a0f03a90 | ||
|
|
5a27aa0366 | ||
|
|
c99a55dc54 | ||
|
|
30d50f15d1 | ||
|
|
ab940acfb5 | ||
|
|
719b1fdd22 | ||
|
|
1fa2e116b3 | ||
|
|
35359f3575 | ||
|
|
40237fae72 | ||
|
|
ad6a3c3147 |
No files matched your search
@@ -11,14 +11,14 @@ body:
|
||||
value: |
|
||||
Please include enough detail for someone else to reproduce the bug.
|
||||
|
||||
Use any language you are comfortable with. The maintainer can read Russian and English directly, so either is preferred. If you are more comfortable in another language, use it.
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Mocktail version
|
||||
label: Nightcap version
|
||||
description: Provide the release version, package version, or commit hash.
|
||||
placeholder: "For example: v0.4.0 or 1a2b3c4"
|
||||
validations:
|
||||
@@ -37,7 +37,7 @@ body:
|
||||
id: installation
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: Tell us how Mocktail was installed or built.
|
||||
description: Tell us how Nightcap was installed or built.
|
||||
placeholder: "For example: AppImage, Flatpak, AUR, or built from source"
|
||||
validations:
|
||||
required: true
|
||||
@@ -56,7 +56,7 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: List the smallest reliable sequence of actions that triggers the problem.
|
||||
placeholder: |
|
||||
1. Start Mocktail with ...
|
||||
1. Start Nightcap with ...
|
||||
2. Open ...
|
||||
3. Select ...
|
||||
4. Observe ...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Feature request
|
||||
description: Suggest an improvement or a new capability for Mocktail
|
||||
description: Suggest an improvement or a new capability for Nightcap
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
@@ -10,9 +11,9 @@ body:
|
||||
value: |
|
||||
Describe what you want to change and why it would be useful.
|
||||
|
||||
Use any language you are comfortable with. The maintainer can read Russian and English directly, so either is preferred. If you are more comfortable in another language, use it.
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
@@ -26,7 +27,7 @@ body:
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe how you would like Mocktail to behave.
|
||||
description: Describe how you would like Nightcap to behave.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Briefly describe what this pull request adds, changes, or removes. Write it yourself and do not use AI-generated text. Link the issue with "Closes #123" or "Fixes #123". -->
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Briefly describe what this pull request adds, changes, or removes. Link the issue with "Closes #123" or "Fixes #123". -->
|
||||
|
||||
Closes #
|
||||
@@ -8,22 +8,32 @@ on:
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- '.gitmodules'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -65,3 +75,54 @@ jobs:
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
bash tests/anylinux_dependencies_test.sh .
|
||||
|
||||
native-build:
|
||||
name: Native build (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-26.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-26.04-arm
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
build-essential binutils cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
|
||||
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
|
||||
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
|
||||
libplacebo-dev libpng-dev zlib1g-dev \
|
||||
xvfb xauth libgl1-mesa-dri jq patchelf
|
||||
|
||||
- name: Build and check native executables
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = "${{ matrix.arch }}"
|
||||
cmake -S . -B build-ci -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
|
||||
cmake --build build-ci --parallel 4
|
||||
./build-ci/mocktail --help
|
||||
./build-ci/mocktail_updater status
|
||||
|
||||
- name: Run C++ and integration tests
|
||||
shell: bash
|
||||
env:
|
||||
SDL_AUDIODRIVER: dummy
|
||||
SDL_VIDEODRIVER: x11
|
||||
LIBGL_ALWAYS_SOFTWARE: '1'
|
||||
run: |
|
||||
xvfb-run -a ctest --test-dir build-ci --output-on-failure \
|
||||
--parallel 4 --timeout 120
|
||||
+93
-1
@@ -6,6 +6,96 @@ GitHub are taken from the matching section here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.3] - 2026-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- Joining a friend from a Roblox "follow user" link now works. Nightcap looks
|
||||
up which game they are in and joins it.
|
||||
- Nix users get a desktop entry for the app.
|
||||
|
||||
### Changed
|
||||
|
||||
- Roblox textures now decode about 3 times faster, which shortens the stalls
|
||||
when a place loads and when new parts of the map stream in. Thanks to
|
||||
@glook9001.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `make update-roblox` no longer deletes your previous Roblox install when an
|
||||
import fails partway through. If a step fails, the old files are put back,
|
||||
or kept in a folder whose path is printed. Thanks to @kimlherme.
|
||||
- A failed Roblox update now restores your previous version's records instead
|
||||
of leaving them half switched. A damaged version file is rejected instead of
|
||||
being half read, and a temporary failure to start the startup check no longer
|
||||
marks a working Roblox build as broken for good. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when Roblox clears its audio queue while sound is
|
||||
still playing, and a memory leak in lists of objects passed to Roblox's Java
|
||||
code. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when the text overlay shuts down, a freeze when Roblox
|
||||
releases the mouse lock while another release is already in progress, and
|
||||
network lookups that misread Roblox's options. Thanks to @kimlherme.
|
||||
- Fixed the sky sometimes rendering with the wrong textures in the Vulkan
|
||||
renderer.
|
||||
- Clicking while a Roblox text box has focus no longer closes it. Only Roblox
|
||||
decides when a click ends typing.
|
||||
- Going to another page in the web view, such as Friends from a profile, no
|
||||
longer closes the web view. It crashed on every page change on Wayland
|
||||
systems whose GL setup GTK could not use.
|
||||
- Fixed a crash when Roblox's saved dark or light theme setting was missing or
|
||||
damaged.
|
||||
- The Flatpak build now detects an NVIDIA driver from inside its sandbox, so it
|
||||
uses X11 instead of native Wayland where both are available. This avoids
|
||||
Wayland hangs and display disconnects.
|
||||
|
||||
## [0.3.2] - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Performance recordings made with `--profile` now show how busy the CPU was
|
||||
during each frame, which makes it easier to tell what caused a stutter.
|
||||
- Experimental support for ARM64 (`aarch64`) Linux, running Roblox's Android
|
||||
`arm64-v8a` client.
|
||||
|
||||
### Fixed
|
||||
|
||||
- When Roblox asks for a Google Play device check during login, Nightcap now
|
||||
answers right away that it isn't supported. Before, the "Verifying you're
|
||||
not a bot" screen waited about a minute for an answer and then sent you
|
||||
back to the login screen.
|
||||
- Solving a login challenge now closes its window. Before, Roblox's
|
||||
"challenge completed" signal was ignored and the window stayed open.
|
||||
Thanks to @glook9001.
|
||||
- Login no longer stalls after you pass a verification check. Before, Roblox
|
||||
only saw the page's "challenge completed" signal after a one-minute
|
||||
timeout, so it gave up and the login failed. On a Roblox build Nightcap
|
||||
can't handle this way, the log says "WebViewProtocol getter not
|
||||
recognized" and web pages behave as before.
|
||||
- Login check windows and other web pages get 5 seconds to start instead of
|
||||
3, so a slow start no longer closes them. The log says how long each one
|
||||
took to open.
|
||||
- Less stutter while joining a game and while textures load in. In Blade
|
||||
Ball, loading hitches dropped by about half, and the worst one went from
|
||||
0.8 seconds to 0.55 seconds.
|
||||
- Clicking Play on a private server now joins right away. Before, nothing
|
||||
happened until you clicked a sidebar tab like Home.
|
||||
- Middle click now reaches Roblox as the middle mouse button.
|
||||
- Scrolling follows your system's natural scrolling setting.
|
||||
- Fixed a crash on some x86_64 hosts when Roblox asked about a thread other
|
||||
than the one making the call.
|
||||
- Roblox updates no longer mix up the ARM64 and x86_64 compatibility profiles
|
||||
for the same version.
|
||||
- A partial or out-of-range user id in `MOCKTAIL_ROBLOX_USER_ID` or
|
||||
`MOCKTAIL_LOCAL_STORAGE_CURRENT_USER_ID` is now ignored instead of being
|
||||
partly read.
|
||||
|
||||
### Changed
|
||||
|
||||
- `make flatpak` and `scripts/build_flatpak.sh` now build with 2 jobs by
|
||||
default instead of 4, so a local Flatpak build is less likely to run out
|
||||
of memory. Set `FLATPAK_JOBS` or `MOCKTAIL_FLATPAK_JOBS` to use more.
|
||||
Thanks to @glook9001.
|
||||
|
||||
## [0.3.1] - 2026-09-20
|
||||
|
||||
### Added
|
||||
@@ -190,7 +280,9 @@ First Nightcap release, forked from komaruworld/mocktail.
|
||||
- The AppImage build no longer fails on stale checksums for the slimmed
|
||||
Arch packages.
|
||||
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.1...HEAD
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.3...HEAD
|
||||
[0.3.3]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.2
|
||||
[0.3.1]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.1
|
||||
[0.3.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.0
|
||||
[0.2.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.2.0
|
||||
|
||||
+17
-4
@@ -10,7 +10,7 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 1.0.4
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
DESCRIPTION "Android Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
@@ -249,6 +249,9 @@ mocktail_apply_compile_options(mocktail_updater)
|
||||
|
||||
add_library(mocktail_legacy_jnivm STATIC
|
||||
src/jnivm/jnivm.cc
|
||||
src/jnivm/jni_references.cc
|
||||
src/jnivm/jni_fields.cc
|
||||
src/jnivm/jni_strings.cc
|
||||
)
|
||||
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
|
||||
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
|
||||
@@ -285,9 +288,13 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/roblox_capability_resolver.cc
|
||||
src/runtime/roblox_experience_launch_bridge.cc
|
||||
src/runtime/roblox_launch_uri.cc
|
||||
src/runtime/roblox_presence_resolver.cc
|
||||
src/runtime/roblox_game_session_native_adapter.cc
|
||||
src/runtime/roblox_web_view_bridge.cc
|
||||
src/runtime/roblox_permissions_bridge.cc
|
||||
src/runtime/roblox_account_bridge.cc
|
||||
src/runtime/roblox_message_bus_request_bridge.cc
|
||||
src/runtime/roblox_call_protocol_bridge.cc
|
||||
src/runtime/payload_update_preflight.cc
|
||||
src/runtime/process_launch_policy.cc
|
||||
src/runtime/process_diagnostics.cc
|
||||
@@ -385,7 +392,8 @@ 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_include_directories(mocktail_webview_helper_policy PUBLIC src
|
||||
PRIVATE include)
|
||||
target_link_libraries(mocktail_webview_helper_policy PUBLIC
|
||||
PkgConfig::WEBKITGTK6
|
||||
nlohmann_json::nlohmann_json
|
||||
@@ -472,7 +480,7 @@ target_link_libraries(mocktail_window PUBLIC
|
||||
Mocktail::SdlVulkanWsi
|
||||
SDL3::SDL3
|
||||
${CMAKE_DL_LIBS}
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay Mocktail::GraphicsFoundation
|
||||
)
|
||||
|
||||
add_library(mocktail_roblox_output_device_runtime STATIC
|
||||
@@ -650,6 +658,11 @@ target_link_options(mocktail PRIVATE
|
||||
)
|
||||
|
||||
# Native FreeBSD helper for Linuxulator's unsupported DF socket option.
|
||||
# The helper is x86-64 assembly; other host architectures have nothing to
|
||||
# assemble.
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
|
||||
set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF)
|
||||
endif()
|
||||
option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER
|
||||
"Build the native FreeBSD Linuxulator socket helper" ON)
|
||||
if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER)
|
||||
@@ -811,7 +824,7 @@ set(CPACK_PACKAGE_CONTACT "vii <coderdayton14@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/CoderDayton/nightcap")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION
|
||||
"Runs the Android x86-64 Roblox client on Linux."
|
||||
"Runs the Android Roblox client on Linux."
|
||||
)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
|
||||
|
||||
@@ -42,6 +42,18 @@ attach `latest.log`:
|
||||
|
||||
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
|
||||
|
||||
## Can I run multiple Roblox instances?
|
||||
|
||||
No. Nightcap won't add multi-instance launching. It would mean working around
|
||||
Roblox's restrictions on running several clients. The [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use)
|
||||
prohibit bypassing technical protections, and violating the terms can get your
|
||||
account suspended.
|
||||
|
||||
Bloxstrap brought the option back in [v2.9.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.9.0)
|
||||
with a "use at your own risk" warning, then removed it in
|
||||
[v2.10.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.10.0)
|
||||
after Roblox added measures against running multiple clients.
|
||||
|
||||
## Can I play in VR?
|
||||
|
||||
VR is experimental and lives on upstream's `vr` branch, which does not
|
||||
|
||||
@@ -21,7 +21,7 @@ ANYLINUX_APPIMAGETOOL ?= appimagetool
|
||||
PREFIX ?= /usr
|
||||
FLATPAK_BUILD_DIR ?= build-flatpak
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
FLATPAK_JOBS ?= 4
|
||||
FLATPAK_JOBS ?= 2
|
||||
|
||||
define build_native_runtime
|
||||
@git submodule update --init --recursive
|
||||
@@ -130,8 +130,8 @@ auto-run: ## Build/run loop with crash summaries
|
||||
run-gles: build ## Run strict system EGL/OpenGL ES 3 without Vulkan fallback
|
||||
@$(BINARY) --graphics opengl
|
||||
|
||||
run-angle: ## Run with ANGLE/Vulkan compatibility backend
|
||||
@MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan ./scripts/run_sober.sh
|
||||
run-angle: build ## Run with ANGLE/Vulkan compatibility backend
|
||||
@$(BINARY) --graphics angle-vulkan
|
||||
|
||||
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
|
||||
@./scripts/update_roblox_payload.sh
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
|
||||
# Nightcap
|
||||
|
||||
[](https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml)
|
||||
[](https://github.com/CoderDayton/nightcap/stargazers)
|
||||
[](LICENSE)
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/io.github.CoderDayton.nightcap.svg" alt="Nightcap logo" width="128">
|
||||
<h1 style="margin-top: 5px;">
|
||||
Nightcap
|
||||
<br>
|
||||
<a href="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml"><img src="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<a href="https://github.com/CoderDayton/nightcap/stargazers"><img src="https://img.shields.io/github/stars/CoderDayton/nightcap?style=flat&logo=github" alt="Stars"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License"></a>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
Nightcap is Mocktail, tuned for playing on a real PC. It runs the Android
|
||||
`x86_64` Roblox client on Linux, including a Linux userspace hosted by
|
||||
@@ -68,7 +73,7 @@ Each release also carries a DEB, an RPM, and a pacman package, with a
|
||||
[releases page](https://github.com/CoderDayton/nightcap/releases/latest):
|
||||
|
||||
```bash
|
||||
sudo apt install ./nightcap_<version>-1_amd64.deb
|
||||
sudo apt install ./nightcap_<version>_amd64.deb
|
||||
sudo dnf install ./nightcap-<version>-1.x86_64.rpm
|
||||
sudo pacman -U nightcap-<version>-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
@@ -90,6 +95,12 @@ The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Nightcap. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
A leftover `.previous_good-recovery.json` in the payload store marks pending
|
||||
recovery after a publication or restoration failure and blocks further payload
|
||||
publication and garbage collection. Check `current.json` and `previous_good.json`
|
||||
against the saved recovery state and fix the underlying problem before manually
|
||||
removing the marker.
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||
@@ -192,6 +203,11 @@ Use `sudo make install` instead to install system-wide under `/usr`. To make
|
||||
Roblox website links open the build tree copy without installing, run
|
||||
`make register-url-handler`.
|
||||
|
||||
To build and install the Flatpak from source, install `flatpak-builder` and run
|
||||
`make flatpak`. It builds with 2 parallel jobs to keep memory use low. Pass
|
||||
`FLATPAK_JOBS=N` to `make flatpak`, or set `MOCKTAIL_FLATPAK_JOBS=N` for
|
||||
`scripts/build_flatpak.sh`, to change that.
|
||||
|
||||
To run the same checks as CI before each commit and push, install
|
||||
[lefthook](https://github.com/evilmartians/lefthook) and run once:
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
"version_name": "2.721.1108",
|
||||
"version_code": 2350,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
|
||||
"status": "legacy-researched",
|
||||
"default_allowed": false,
|
||||
@@ -15,6 +16,7 @@
|
||||
{
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -35,6 +37,7 @@
|
||||
{
|
||||
"version_name": "2.727.1199",
|
||||
"version_code": 2628,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -46,6 +49,7 @@
|
||||
{
|
||||
"version_name": "2.734.917",
|
||||
"version_code": 2908,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
@@ -66,6 +70,19 @@
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"abi": "arm64-v8a",
|
||||
"elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching."
|
||||
},
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
|
||||
+14
-1
@@ -46,15 +46,28 @@ Each slice is named after the call it times, on the thread that made it.
|
||||
| `submit` | `vkQueueSubmit`, `vkQueueSubmit2`, `vkQueueSubmit2KHR` | `submits` |
|
||||
| `texture` | `etc2 decode`: CPU decode of ETC2 textures. On a device with a timeline semaphore this runs on the decode dispatcher, not the submitting thread | `uploads`, `compressed_bytes`, `decoded_bytes` |
|
||||
| `texture` | `etc2 gather`: the part of an asynchronous upload that stays on the submitting thread. Absent when the decode ran inline | `uploads`, `compressed_bytes`, `decoded_bytes`, `ticket` |
|
||||
| `texture` | `etc2 staging create`: creating a staging block for ETC2 uploads, when no block has room left | `bytes` |
|
||||
| `texture` | `etc2 staging destroy`: destroying empty staging blocks beyond the four kept idle | `buffers` |
|
||||
| `texture` | `etc2 release wait`: a command buffer reset waiting for its uploads' decode. Only waits of 0.1 ms or more are recorded | `ticket` |
|
||||
| `memory` | `vkAllocateMemory` | `bytes`, `type` (memory type index), `result` |
|
||||
| `memory` | `vkFreeMemory`, `vkUnmapMemory`: both include waiting for ETC2 decodes that still read that memory | |
|
||||
| `memory` | `vkMapMemory`, `vkCreateImage`, `vkDestroyImage` | |
|
||||
| `pipeline` | `vkCreateGraphicsPipelines`, `vkCreateComputePipelines` | `count`, `cache` (1 when a pipeline cache was passed), `result` |
|
||||
| `pipeline` | `vkCreateShaderModule` | `bytes`, `result` |
|
||||
| `pipeline` | `vkCreatePipelineCache` | `initial_bytes`, `result` |
|
||||
| `pump` | `nativeCallMessagesFromMainThread`: the engine's main-thread step, on the host main thread. Only calls of 20 µs or more are recorded; the empty polls between them are not. | |
|
||||
| `pump` | `nativeCallMessagesFromMainThread`: the engine's main-thread step, on the host main thread. Only calls of 20 µs or more are recorded; the empty polls between them are not. | `cpu_us`: CPU time the main thread used during the call. Far below the slice's duration means the step spent it waiting |
|
||||
|
||||
The `frame interval (ms)` counter is the time between successive adapter
|
||||
presents. `vkQueuePresentKHR` time minus `host present` time is the adapter's
|
||||
own cost per frame.
|
||||
|
||||
Two more counters cover the same interval. `present thread cpu (ms)` is the
|
||||
CPU time the thread that presents used in it, and is absent for a frame
|
||||
presented from a different thread than the one before. `process cpu (ms)` is
|
||||
the CPU time of every thread together, so dividing it by the frame interval
|
||||
gives the number of cores busy. A long frame with little present-thread CPU
|
||||
was spent waiting.
|
||||
|
||||
## The benchmark runs
|
||||
|
||||
Run every scenario before and after the change, on the same build type
|
||||
|
||||
+26
-35
@@ -16,7 +16,7 @@ Goal: a small GTK4 settings window, reachable from the app menu and from a
|
||||
- Input tab: the `input.raw_mouse` toggle and controller options.
|
||||
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
|
||||
- Discord tab: every field in the `discord_rpc` block, so a friend can see
|
||||
what you are playing and you can switch it all off in one click.
|
||||
what you are playing and you can switch it all off in one click.
|
||||
- Changes apply on next launch. A restart button restarts Nightcap.
|
||||
|
||||
Done when every option in `config/mocktail.example.yaml` that a player would
|
||||
@@ -29,62 +29,53 @@ already hot-plug and release their held inputs when unplugged. One rough edge
|
||||
is left.
|
||||
|
||||
- Correct button glyphs for the connected pad. `SDL_GetGamepadType` is only
|
||||
read for Xbox and PlayStation pads, so a Switch Pro, a GameCube pad, or any
|
||||
generic pad SDL reports as `SDL_GAMEPAD_TYPE_STANDARD` reaches Roblox as
|
||||
type 0 and draws generic glyphs. `SDL_GetRealGamepadType`, and the vendor
|
||||
and product IDs, are available and unused.
|
||||
read for Xbox and PlayStation pads, so a Switch Pro, a GameCube pad, or any
|
||||
generic pad SDL reports as `SDL_GAMEPAD_TYPE_STANDARD` reaches Roblox as
|
||||
type 0 and draws generic glyphs. `SDL_GetRealGamepadType`, and the vendor
|
||||
and product IDs, are available and unused.
|
||||
|
||||
Done when a plugged-in pad shows its own buttons.
|
||||
|
||||
## 3. First-load stutter
|
||||
|
||||
The `--profile` trace now shows shader compiles and pipeline creation, so
|
||||
the stutter on first entering an experience can be measured.
|
||||
|
||||
- Cut the stutter the trace shows, and prove it with a before and after
|
||||
trace per [BENCHMARKING.md](BENCHMARKING.md).
|
||||
|
||||
Done when entering a fresh experience no longer drops frames on shader
|
||||
compiles.
|
||||
|
||||
## Shipped
|
||||
|
||||
- Lower input latency: frames are presented as soon as they are ready.
|
||||
- Controllers hot-plug without a restart, and a pad that is unplugged mid-game
|
||||
releases its held buttons and sticks instead of leaving them stuck.
|
||||
releases its held buttons and sticks instead of leaving them stuck.
|
||||
- Mouse look keeps responding after a long turn, instead of going dead until
|
||||
the pointer travels back from past the screen edge.
|
||||
the pointer travels back from past the screen edge.
|
||||
- Optional raw mouse input: `input.raw_mouse` in `config.yaml` sends look
|
||||
deltas in host units, so fractional display scaling no longer changes
|
||||
first-person sensitivity.
|
||||
deltas in host units, so fractional display scaling no longer changes
|
||||
first-person sensitivity.
|
||||
- Small textures upscaled 4x by default, with PNG overrides in
|
||||
`~/.config/mocktail/textures`.
|
||||
`~/.config/mocktail/textures`.
|
||||
- ETC2 textures decoded on desktop GPUs without stutter.
|
||||
- No more error 319 kicks after joining a server.
|
||||
- Roblox sees the real host RAM and screen size.
|
||||
- Desktop launcher that works on Wayland and finds the installed binary.
|
||||
- Discord Rich Presence on by default, with the experience name, creator,
|
||||
elapsed time, the experience icon with a Roblox badge, and a join button
|
||||
when the server is public. See the [FAQ](../FAQ.md) to switch it off.
|
||||
elapsed time, the experience icon with a Roblox badge, and a join button
|
||||
when the server is public. See the [FAQ](../FAQ.md) to switch it off.
|
||||
- Custom Discord presence: title, details, state, and large and small icons
|
||||
from `config.yaml`, with `{place_name}`, `{place_icon}`, and
|
||||
`{creator_name}` placeholders.
|
||||
from `config.yaml`, with `{place_name}`, `{place_icon}`, and
|
||||
`{creator_name}` placeholders.
|
||||
- `--profile` writes a Chrome-format trace of the Vulkan adapter's work,
|
||||
including shader and pipeline creation, readable in `ui.perfetto.dev`.
|
||||
[BENCHMARKING.md](BENCHMARKING.md) fixes the experiences to measure and
|
||||
`scripts/summarize_profile_trace.py` builds the before and after table.
|
||||
including shader and pipeline creation, readable in `ui.perfetto.dev`.
|
||||
[BENCHMARKING.md](BENCHMARKING.md) fixes the experiences to measure and
|
||||
`scripts/summarize_profile_trace.py` builds the before and after table.
|
||||
- Texture uploads no longer stall the frame they land on: ETC2 decode and
|
||||
resampling run on a persistent worker pool.
|
||||
resampling run on a persistent worker pool.
|
||||
- Less stutter while joining an experience and while textures load in. See
|
||||
the [changelog](../CHANGELOG.md) for the numbers.
|
||||
- The host main thread rests between engine polls instead of spinning, and
|
||||
GameMode's core pinning is released, so Roblox's workers spread across
|
||||
every core. See [PERFORMANCE.md](PERFORMANCE.md).
|
||||
GameMode's core pinning is released, so Roblox's workers spread across
|
||||
every core. See [PERFORMANCE.md](PERFORMANCE.md).
|
||||
- Own app ID `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream.
|
||||
upstream.
|
||||
- AppImage and signed Flatpak releases on every `v*` tag, with the Flatpak
|
||||
repo on GitHub Pages.
|
||||
repo on GitHub Pages.
|
||||
|
||||
## Not planned
|
||||
|
||||
- Replacing the `mocktail` binary and config names. Keeping them makes
|
||||
upstream merges clean.
|
||||
- DEB, RPM, and AUR packages. Upstream ships those for unmodified Mocktail.
|
||||
upstream merges clean.
|
||||
|
||||
@@ -49,6 +49,17 @@
|
||||
gdb
|
||||
];
|
||||
|
||||
desktopItems = [
|
||||
(pkgs.makeDesktopItem {
|
||||
name = "Mocktail";
|
||||
exec = "mocktail";
|
||||
icon = "mocktail";
|
||||
desktopName = "Mocktail";
|
||||
comment = "Experimental Roblox compatibility runtime for Linux and FreeBSD";
|
||||
categories = [ "Game" ];
|
||||
})
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath package.buildInputs}"
|
||||
|
||||
|
||||
@@ -33,6 +33,23 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled);
|
||||
// guest layout explicit instead of exposing the incompatible host libc type.
|
||||
using MocktailBionicMutexAttr = int64_t;
|
||||
|
||||
// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host
|
||||
// glibc layout is opaque and wider on aarch64, so guest buffers must be read
|
||||
// and written through this struct only.
|
||||
struct MocktailBionicPthreadAttr {
|
||||
std::uint32_t flags;
|
||||
void* stack_base;
|
||||
std::size_t stack_size;
|
||||
std::size_t guard_size;
|
||||
std::int32_t sched_policy;
|
||||
std::int32_t sched_priority;
|
||||
char reserved[16];
|
||||
};
|
||||
static_assert(sizeof(MocktailBionicPthreadAttr) == 56);
|
||||
|
||||
inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1;
|
||||
inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2;
|
||||
|
||||
extern "C" {
|
||||
|
||||
void mocktail_set_current_jni_env(void* env);
|
||||
@@ -84,6 +101,19 @@ int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
|
||||
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
|
||||
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
|
||||
|
||||
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
|
||||
size_t stack_size);
|
||||
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
|
||||
int detach_state);
|
||||
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
|
||||
const struct sched_param* parameters);
|
||||
int mocktail_pthread_getattr_np(pthread_t thread,
|
||||
MocktailBionicPthreadAttr* attr);
|
||||
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
|
||||
void** stack_base, size_t* stack_size);
|
||||
|
||||
extern void* mocktail_gameactivity_on_start_native;
|
||||
extern void* mocktail_gameactivity_on_resume_native;
|
||||
extern void* mocktail_gameactivity_on_surface_created_native;
|
||||
|
||||
@@ -25,11 +25,12 @@ void ConfigureBionicPthreadThreadInitializer(
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
struct MocktailBionicPthreadAttr;
|
||||
|
||||
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument);
|
||||
extern "C" int mocktail_bionic_pthread_create(
|
||||
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument);
|
||||
|
||||
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
|
||||
extern "C" int mocktail_bionic_pthread_setschedparam(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
|
||||
#include <elf.h>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// Android ABI name as it appears in APK lib/ directories and build metadata.
|
||||
inline constexpr std::string_view kGuestAbi = "arm64-v8a";
|
||||
// APK split-config name carrying the guest native libraries.
|
||||
inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a";
|
||||
// File name the payload store uses for the split APK.
|
||||
inline constexpr std::string_view kGuestSplitApkFile =
|
||||
"split_config.arm64_v8a.apk";
|
||||
// sha256 map key holding the split APK digest in payload metadata.
|
||||
inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk";
|
||||
// Short CPU label for device metadata and user-facing strings.
|
||||
inline constexpr std::string_view kGuestCpuName = "aarch64";
|
||||
inline constexpr int kGuestElfMachine = EM_AARCH64;
|
||||
#elif defined(__x86_64__)
|
||||
inline constexpr std::string_view kGuestAbi = "x86_64";
|
||||
inline constexpr std::string_view kGuestApkSplitName = "config.x86_64";
|
||||
inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk";
|
||||
inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk";
|
||||
inline constexpr std::string_view kGuestCpuName = "x86_64";
|
||||
inline constexpr int kGuestElfMachine = EM_X86_64;
|
||||
#else
|
||||
#error "mocktail only supports aarch64 and x86_64 hosts"
|
||||
#endif
|
||||
|
||||
// APK zip entry holding the guest libroblox.so.
|
||||
inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/";
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_
|
||||
@@ -0,0 +1,190 @@
|
||||
#ifndef MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
#define MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
// Roblox's Subsystem<IWebViewProtocol> getter is not exported. The exported
|
||||
// WebViewProtocol.initializeAndroidWebViewProtocol calls it first, into a
|
||||
// stack slot, then reads the protocol from the returned holder:
|
||||
//
|
||||
// x86-64: lea slot(%rbp),%rbx; mov %rbx,%rdi; call getter;
|
||||
// mov (%rbx),%rbx; mov 0x18(%rbx),%rax
|
||||
// AArch64: add x8, sp, #slot; bl getter; ldr xN, [sp, #slot];
|
||||
// ldr x8, [xN, #0x18]
|
||||
//
|
||||
// These functions find that call site in the initializer's first bytes and
|
||||
// check the getter's own code. Only relocated or build-varying fields (stack
|
||||
// slots, frame sizes, PC-relative offsets, the call target) are masked.
|
||||
// Verified on x86_64 2.734.917, 2.736.1408 and 2.738.1397, and on arm64-v8a
|
||||
// 2.738.1397 and 2.740.931.
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Covers the call site in every verified build (x86-64 +0x27, AArch64 +0x34).
|
||||
inline constexpr std::size_t kWebViewProtocolInitializerScanBytes = 128;
|
||||
inline constexpr std::size_t kWebViewProtocolGetterBytesX86_64 = 44;
|
||||
inline constexpr std::size_t kWebViewProtocolGetterBytesArm64 = 60;
|
||||
|
||||
namespace web_view_protocol_contract_detail {
|
||||
|
||||
inline bool MatchesMasked(std::string_view code, std::size_t offset,
|
||||
std::string_view pattern,
|
||||
std::string_view wildcard) {
|
||||
if (offset > code.size() || code.size() - offset < pattern.size()) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < pattern.size(); ++index) {
|
||||
if (wildcard[index] == 'x') continue;
|
||||
if (code[offset + index] != pattern[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// AArch64 instructions are little-endian words.
|
||||
inline std::uint32_t Word(std::string_view code, std::size_t index) {
|
||||
std::uint32_t word = 0;
|
||||
for (std::size_t byte = 0; byte < 4; ++byte) {
|
||||
word |= static_cast<std::uint32_t>(
|
||||
static_cast<unsigned char>(code[index * 4 + byte]))
|
||||
<< (8 * byte);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
// One AArch64 instruction word: bits outside `mask` may vary.
|
||||
struct MaskedWord {
|
||||
std::uint32_t mask;
|
||||
std::uint32_t value;
|
||||
};
|
||||
|
||||
inline constexpr std::uint32_t kExact = 0xffffffffU;
|
||||
// adrp Xd, page: immlo and immhi vary.
|
||||
inline constexpr std::uint32_t kAdrpMask = 0x9f00001fU;
|
||||
// add Xd, Xn, #imm12: imm12 varies.
|
||||
inline constexpr std::uint32_t kAddImmediateMask = 0xffc003ffU;
|
||||
|
||||
} // namespace web_view_protocol_contract_detail
|
||||
|
||||
// Returns the getter address called from the x86-64 initializer at
|
||||
// `address`, or nullopt unless the frame setup and exactly one call site
|
||||
// match.
|
||||
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallX86_64(
|
||||
std::string_view code, std::uintptr_t address) {
|
||||
using web_view_protocol_contract_detail::MatchesMasked;
|
||||
// push %rbp; mov %rsp,%rbp
|
||||
if (!MatchesMasked(code, 0, "\x55\x48\x89\xe5", "....")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
static constexpr std::string_view kCallSite(
|
||||
"\x48\x8d\x5d\x00\x48\x89\xdf\xe8\x00\x00\x00\x00\x48\x8b\x1b\x48\x8b\x43"
|
||||
"\x18",
|
||||
19);
|
||||
static constexpr std::string_view kCallSiteWildcard("...x....xxxx.......");
|
||||
static constexpr std::size_t kCallDisplacement = 8;
|
||||
const std::string_view window =
|
||||
code.substr(0, kWebViewProtocolInitializerScanBytes);
|
||||
std::optional<std::size_t> call_site;
|
||||
for (std::size_t offset = 0; offset < window.size(); ++offset) {
|
||||
if (MatchesMasked(window, offset, kCallSite, kCallSiteWildcard)) {
|
||||
if (call_site.has_value()) return std::nullopt;
|
||||
call_site = offset;
|
||||
}
|
||||
}
|
||||
if (!call_site.has_value()) return std::nullopt;
|
||||
std::int32_t displacement = 0;
|
||||
std::memcpy(&displacement, window.data() + *call_site + kCallDisplacement,
|
||||
sizeof(displacement));
|
||||
const std::uintptr_t next_instruction =
|
||||
address + *call_site + kCallDisplacement + sizeof(displacement);
|
||||
return next_instruction +
|
||||
static_cast<std::uintptr_t>(static_cast<std::intptr_t>(displacement));
|
||||
}
|
||||
|
||||
// The x86-64 getter stores the holder returned by the subsystem's acquire
|
||||
// into its result slot (%rdi) and returns that slot.
|
||||
inline bool HasWebViewProtocolGetterContractX86_64(std::string_view code) {
|
||||
return web_view_protocol_contract_detail::MatchesMasked(
|
||||
code, 0,
|
||||
std::string_view(
|
||||
"\x55\x48\x89\xe5\x53\x50\x48\x89\xfb\x8a\x05\x00\x00\x00\x00\x84\xc0"
|
||||
"\x74\x19\x48\x8d\x3d\x00\x00\x00\x00\xe8\x59\x00\x00\x00\x48\x89\x03"
|
||||
"\x48\x89\xd8\x48\x83\xc4\x08\x5b\x5d\xc3",
|
||||
kWebViewProtocolGetterBytesX86_64),
|
||||
"...........xxxx.......xxxx..................");
|
||||
}
|
||||
|
||||
// Returns the getter address called from the AArch64 initializer at
|
||||
// `address`, or nullopt unless exactly one call site matches.
|
||||
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallArm64(
|
||||
std::string_view code, std::uintptr_t address) {
|
||||
using web_view_protocol_contract_detail::Word;
|
||||
const std::size_t words =
|
||||
code.substr(0, kWebViewProtocolInitializerScanBytes).size() / 4;
|
||||
std::optional<std::uintptr_t> getter;
|
||||
for (std::size_t index = 0; index + 4 <= words; ++index) {
|
||||
const std::uint32_t slot_address = Word(code, index);
|
||||
const std::uint32_t call = Word(code, index + 1);
|
||||
const std::uint32_t handle_load = Word(code, index + 2);
|
||||
const std::uint32_t protocol_load = Word(code, index + 3);
|
||||
// add x8, sp, #slot
|
||||
if ((slot_address & 0xffc003ffU) != 0x910003e8U) continue;
|
||||
// bl getter
|
||||
if ((call & 0xfc000000U) != 0x94000000U) continue;
|
||||
// ldr xN, [sp, #slot]
|
||||
if ((handle_load & 0xffc003e0U) != 0xf94003e0U) continue;
|
||||
const std::uint32_t slot = (slot_address >> 10) & 0xfffU;
|
||||
if (((handle_load >> 10) & 0xfffU) * 8 != slot) continue;
|
||||
// ldr x8, [xN, #0x18]
|
||||
const std::uint32_t handle_register = handle_load & 0x1fU;
|
||||
if (protocol_load != (0xf9400c08U | (handle_register << 5))) continue;
|
||||
if (getter.has_value()) return std::nullopt;
|
||||
// imm26 is a signed word offset from the bl instruction.
|
||||
const std::int64_t imm26 = call & 0x03ffffffU;
|
||||
const std::int64_t words_from_call =
|
||||
(imm26 & 0x02000000) != 0 ? imm26 - 0x04000000 : imm26;
|
||||
getter = address + (index + 1) * 4 +
|
||||
static_cast<std::uintptr_t>(words_from_call * 4);
|
||||
}
|
||||
return getter;
|
||||
}
|
||||
|
||||
// The AArch64 getter saves its result slot (x8) and stores the holder
|
||||
// returned by the subsystem's acquire there.
|
||||
inline bool HasWebViewProtocolGetterContractArm64(std::string_view code) {
|
||||
using namespace web_view_protocol_contract_detail;
|
||||
static constexpr MaskedWord kGetter[] = {
|
||||
{kExact, 0xa9be7bfdU}, // stp x29, x30, [sp, #-0x20]!
|
||||
{kExact, 0xf9000bf3U}, // str x19, [sp, #0x10]
|
||||
{kExact, 0x910003fdU}, // mov x29, sp
|
||||
{kAdrpMask, 0x90000009U}, // adrp x9, guard
|
||||
{kAddImmediateMask, 0x91000129U}, // add x9, x9, :lo12:guard
|
||||
{kExact, 0xaa0803f3U}, // mov x19, x8
|
||||
{kExact, 0x08dffd29U}, // ldarb w9, [x9]
|
||||
{kExact, 0x36000109U}, // tbz w9, #0, initialize
|
||||
{kAdrpMask, 0x90000000U}, // adrp x0, holder
|
||||
{kAddImmediateMask, 0x91000000U}, // add x0, x0, :lo12:holder
|
||||
{kExact, 0x94000017U}, // bl acquire
|
||||
{kExact, 0xf9000260U}, // str x0, [x19]
|
||||
{kExact, 0xf9400bf3U}, // ldr x19, [sp, #0x10]
|
||||
{kExact, 0xa8c27bfdU}, // ldp x29, x30, [sp], #0x20
|
||||
{kExact, 0xd65f03c0U}, // ret
|
||||
};
|
||||
static_assert(sizeof(kGetter) / sizeof(kGetter[0]) * 4 ==
|
||||
kWebViewProtocolGetterBytesArm64);
|
||||
if (code.size() < kWebViewProtocolGetterBytesArm64) return false;
|
||||
for (std::size_t index = 0; index < sizeof(kGetter) / sizeof(kGetter[0]);
|
||||
++index) {
|
||||
if ((Word(code, index) & kGetter[index].mask) != kGetter[index].value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
@@ -48,9 +48,15 @@ struct PlatformIdentity {
|
||||
std::string manufacturer = "Mocktail";
|
||||
std::string model = "Mocktail Headless";
|
||||
std::string brand = "Mocktail";
|
||||
#if defined(__aarch64__)
|
||||
std::string device_code = "linux-arm64";
|
||||
std::string device_sku = "mocktail-arm64";
|
||||
std::string soc_model = "aarch64";
|
||||
#else
|
||||
std::string device_code = "linux-x86_64";
|
||||
std::string device_sku = "mocktail-x86_64";
|
||||
std::string soc_model = "x86_64";
|
||||
#endif
|
||||
};
|
||||
|
||||
struct RobloxCredentialView {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#if defined(__x86_64__)
|
||||
#include <xmmintrin.h>
|
||||
#endif
|
||||
#if defined(__aarch64__)
|
||||
#include <cstdint>
|
||||
#endif
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
@@ -22,16 +25,23 @@ class FmodThreadFloatingPointMode final {
|
||||
delete;
|
||||
|
||||
bool Enable(const char* thread_name) noexcept {
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__x86_64__) || defined(__aarch64__)
|
||||
if (active_ || thread_name == nullptr) return false;
|
||||
const std::string_view name(thread_name);
|
||||
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
|
||||
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
|
||||
return false;
|
||||
}
|
||||
#if defined(__x86_64__)
|
||||
const unsigned int control = _mm_getcsr();
|
||||
original_mode_ = control & kModeMask;
|
||||
_mm_setcsr(control | kModeMask);
|
||||
#else
|
||||
std::uint64_t control;
|
||||
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
|
||||
original_mode_ = control & kModeMask;
|
||||
__asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask));
|
||||
#endif
|
||||
active_ = true;
|
||||
return true;
|
||||
#else
|
||||
@@ -41,11 +51,18 @@ class FmodThreadFloatingPointMode final {
|
||||
}
|
||||
|
||||
void Restore() noexcept {
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__x86_64__) || defined(__aarch64__)
|
||||
if (active_) {
|
||||
// Restore only our two bits, preserving rounding, exception masks and
|
||||
// status flags set by the guest while the thread was attached.
|
||||
#if defined(__x86_64__)
|
||||
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
|
||||
#else
|
||||
std::uint64_t control;
|
||||
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
|
||||
__asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) |
|
||||
original_mode_));
|
||||
#endif
|
||||
active_ = false;
|
||||
}
|
||||
#endif
|
||||
@@ -56,6 +73,13 @@ class FmodThreadFloatingPointMode final {
|
||||
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
|
||||
unsigned int original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#elif defined(__aarch64__)
|
||||
// FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision
|
||||
// path used by some voice DSP filters.
|
||||
static constexpr std::uint64_t kModeMask =
|
||||
(std::uint64_t{1} << 24) | (std::uint64_t{1} << 19);
|
||||
std::uint64_t original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ struct AngleProbeOptions {
|
||||
bool allow_software_device = false;
|
||||
};
|
||||
|
||||
// Checks ANGLE entry points and client extensions without initializing a display.
|
||||
BackendCapability InspectAngleLibraries(const AngleProbeOptions& options);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -24,6 +24,35 @@ struct TraceArg {
|
||||
|
||||
// CLOCK_MONOTONIC in nanoseconds.
|
||||
std::uint64_t TraceClockNanos();
|
||||
// CPU time used so far by the calling thread, in nanoseconds.
|
||||
std::uint64_t TraceThreadCpuNanos();
|
||||
// CPU time used so far by every thread of the process, in nanoseconds.
|
||||
std::uint64_t TraceProcessCpuNanos();
|
||||
|
||||
// CPU time spent between two successive presents.
|
||||
struct FrameCpuSample {
|
||||
double process_ms = 0;
|
||||
// Set only when the previous present came from the same thread; thread CPU
|
||||
// clocks of different threads cannot be subtracted.
|
||||
bool has_thread = false;
|
||||
double thread_ms = 0;
|
||||
};
|
||||
|
||||
// Turns CPU clocks read at each present into per-frame CPU time.
|
||||
// Thread-safe.
|
||||
class FrameCpuSampler final {
|
||||
public:
|
||||
// Returns false for the first present, which ends no frame.
|
||||
bool Sample(std::uint64_t thread_id, std::uint64_t thread_cpu_ns,
|
||||
std::uint64_t process_cpu_ns, FrameCpuSample* sample);
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
bool has_previous_ = false;
|
||||
std::uint64_t previous_thread_id_ = 0;
|
||||
std::uint64_t previous_thread_cpu_ns_ = 0;
|
||||
std::uint64_t previous_process_cpu_ns_ = 0;
|
||||
};
|
||||
|
||||
// Writes the Trace Event Format JSON array read by chrome://tracing and
|
||||
// ui.perfetto.dev. Recording threads only format into a shared buffer; a
|
||||
|
||||
@@ -35,6 +35,10 @@ void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::size_t source_stride_bytes,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::uint8_t* destination);
|
||||
|
||||
// Replacement textures keyed by the hash of a texture's level-0 compressed
|
||||
// bytes. An override lives at <override_dir>/<hash>.png. With a dump
|
||||
|
||||
@@ -47,6 +47,10 @@ inline constexpr std::uint32_t kSmallTextureMaxExtent = 64;
|
||||
inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4;
|
||||
std::uint32_t SmallTextureUpscale(const char* value);
|
||||
|
||||
// Staging for decoded uploads is carved out of host-visible blocks this
|
||||
// large; an upload that needs more gets a block of its own.
|
||||
inline constexpr VkDeviceSize kEtc2StagingBlockBytes = 16 * 1024 * 1024;
|
||||
|
||||
// A timeline semaphore and the value an asynchronous decode will signal.
|
||||
// `semaphore` is VK_NULL_HANDLE when the decode already finished on the
|
||||
// calling thread and the submit needs no extra wait.
|
||||
@@ -61,11 +65,12 @@ struct Etc2SubmitWait {
|
||||
// that is filled by decoding the application's staging bytes when the
|
||||
// command buffer is submitted. Every submit decodes again, as a native copy
|
||||
// re-reads its source on each execution.
|
||||
// Staging buffers live until their command buffer is begun, reset or freed.
|
||||
// Staging ranges live until their command buffer is begun, reset or freed.
|
||||
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
|
||||
class VulkanEtc2Emulation final {
|
||||
public:
|
||||
VulkanEtc2Emulation();
|
||||
explicit VulkanEtc2Emulation(
|
||||
VkDeviceSize staging_block_bytes = kEtc2StagingBlockBytes);
|
||||
~VulkanEtc2Emulation();
|
||||
VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete;
|
||||
VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete;
|
||||
|
||||
@@ -47,8 +47,8 @@ struct RobloxThemeCacheResult {
|
||||
};
|
||||
|
||||
// Reads the current account's Roblox theme without changing appStorage. A
|
||||
// missing value leaves dark_theme empty so the caller can use Roblox's own
|
||||
// startup default.
|
||||
// missing or unrecognized value falls back to AuthenticatedTheme, then the
|
||||
// caller's startup default.
|
||||
RobloxThemeCacheResult ReadRobloxThemeCache(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
std::int64_t authenticated_user_id);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_ACCOUNT_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_ACCOUNT_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Exact JNI entrypoints behind the APK AccountProtocol device-integrity
|
||||
// handlers: JNIAccountProtocol name getters and MessageBus raw handlers.
|
||||
struct RobloxAccountProtocolSymbols {
|
||||
using GetStringFn = jstring (*)(JNIEnv*, jclass);
|
||||
|
||||
GetStringFn get_protocol_name = nullptr;
|
||||
GetStringFn get_device_integrity_available_method_name = nullptr;
|
||||
GetStringFn get_integrity_token_method_name = nullptr;
|
||||
GetStringFn get_support_key = nullptr;
|
||||
GetStringFn get_token_key = nullptr;
|
||||
GetStringFn get_result_key = nullptr;
|
||||
void (*set_request_handler_raw)(JNIEnv*, jobject, jstring, jstring,
|
||||
jobject) = nullptr;
|
||||
void (*clear_request_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return get_protocol_name && get_device_integrity_available_method_name &&
|
||||
get_integrity_token_method_name && get_support_key &&
|
||||
get_token_key && get_result_key && set_request_handler_raw &&
|
||||
clear_request_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxAccountProtocolObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void* context = nullptr;
|
||||
jobject (*create_request_handler)(void*, std::shared_ptr<void>,
|
||||
std::string (*)(void*, JNIEnv*,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_request_handler)(void*, jobject) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus && context && create_request_handler &&
|
||||
clear_request_handler;
|
||||
}
|
||||
};
|
||||
|
||||
// Answers AccountProtocol device-integrity requests for a host that cannot
|
||||
// produce Play Integrity tokens. No integrity token is ever produced.
|
||||
//
|
||||
// DeviceIntegrityAvailable reports no support. The APK handler always reports
|
||||
// support, so this answer differs from Android: it tells Roblox's challenge
|
||||
// flow up front that no token can come, instead of letting it request one.
|
||||
//
|
||||
// GetIntegrityToken returns at once with an empty token and
|
||||
// TOKEN_PROVIDER_UNINITIALIZED, the APK's answer when its Play token provider
|
||||
// never initialized. The APK answers INVALID_NONCE for a request without a
|
||||
// requestHash; this bridge never inspects the request, so it cannot.
|
||||
// LuaApp bounds each token request with its
|
||||
// DeviceIntegrityNativeTimeoutMilliseconds flag; an immediate answer keeps a
|
||||
// login challenge from sitting out that bound.
|
||||
class RobloxAccountBridge final {
|
||||
public:
|
||||
RobloxAccountBridge(JniEnvironmentProvider environment,
|
||||
RobloxAccountProtocolSymbols symbols,
|
||||
RobloxAccountProtocolObjects objects);
|
||||
~RobloxAccountBridge();
|
||||
RobloxAccountBridge(const RobloxAccountBridge&) = delete;
|
||||
RobloxAccountBridge& operator=(const RobloxAccountBridge&) = delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxAccountProtocolSymbols symbols_;
|
||||
const RobloxAccountProtocolObjects objects_;
|
||||
// Serializes lifecycle only. Native dispatch never holds this mutex.
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_ACCOUNT_BRIDGE_H_
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_CALL_PROTOCOL_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_CALL_PROTOCOL_BRIDGE_H_
|
||||
|
||||
#include "runtime/roblox_message_bus_request_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// In-experience CoreScripts query the app's cross-experience call state before
|
||||
// initializing voice. Mocktail does not start or accept cross-experience calls,
|
||||
// so that state is Idle. This query neither joins voice nor opens a microphone.
|
||||
class RobloxCallProtocolBridge final {
|
||||
public:
|
||||
RobloxCallProtocolBridge(JniEnvironmentProvider environment,
|
||||
RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects);
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
RobloxMessageBusRequestBridge bridge_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
@@ -14,9 +15,11 @@
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_call_protocol_bridge.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
#include "runtime/roblox_fresh_game_launch_controller.h"
|
||||
#include "runtime/roblox_account_bridge.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
#include "runtime/webview_roblox_cookie.h"
|
||||
@@ -95,7 +98,9 @@ struct RobloxExperienceSurfaceProvider {
|
||||
void* context = nullptr;
|
||||
GameSurface (*snapshot)(void* context) = nullptr;
|
||||
|
||||
bool valid() const { return snapshot != nullptr; }
|
||||
bool valid() const {
|
||||
return snapshot != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Production composition for the APK platform protocols and ExperienceProtocol
|
||||
@@ -120,7 +125,8 @@ class RobloxExperienceComposition final {
|
||||
RobloxExperienceSurfaceProvider surface_provider = {},
|
||||
RobloxExperiencePresenceObserver presence_observer = {},
|
||||
bool clear_persisted_web_view_cookie = false,
|
||||
bool microphone_enabled = false);
|
||||
bool microphone_enabled = false,
|
||||
RobloxAccountProtocolSymbols account_symbols = {});
|
||||
~RobloxExperienceComposition();
|
||||
|
||||
RobloxExperienceComposition(const RobloxExperienceComposition&) = delete;
|
||||
@@ -162,6 +168,7 @@ class RobloxExperienceComposition final {
|
||||
bool visible = true;
|
||||
bool back_navigation_disabled = false;
|
||||
bool show_domain_as_title = false;
|
||||
bool close_when_challenge_solved = false;
|
||||
};
|
||||
|
||||
static Status DispatchLaunch(void* context,
|
||||
@@ -201,8 +208,11 @@ class RobloxExperienceComposition final {
|
||||
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();
|
||||
// mutate the surface that the other one opened. A nonzero
|
||||
// expected_logical_generation closes only that surface.
|
||||
Status CloseWebSurface(uint64_t expected_logical_generation = 0);
|
||||
// Closes a solved challenge that Roblox left open past its grace period.
|
||||
Status CloseUnclaimedSolvedChallenge();
|
||||
void HandleWebSurfaceExit(uint64_t process_generation);
|
||||
Status Dispatch(const RobloxExperienceLaunchRequest& request);
|
||||
Status PromoteAuthenticatedSession();
|
||||
@@ -229,6 +239,7 @@ class RobloxExperienceComposition final {
|
||||
const RobloxWebViewMessageBusSymbols web_view_symbols_;
|
||||
const RobloxBrowserServiceSymbols browser_service_symbols_;
|
||||
const RobloxPermissionsMessageBusSymbols permissions_symbols_;
|
||||
const RobloxAccountProtocolSymbols account_symbols_;
|
||||
const bool microphone_enabled_;
|
||||
const RobloxGameSessionSymbols game_symbols_;
|
||||
const RobloxExperienceJniFactory jni_factory_;
|
||||
@@ -248,6 +259,9 @@ class RobloxExperienceComposition final {
|
||||
std::unique_ptr<RobloxWebViewBridge> web_view_bridge_;
|
||||
std::unique_ptr<RobloxBrowserServiceBridge> browser_service_bridge_;
|
||||
std::unique_ptr<RobloxPermissionsBridge> permissions_bridge_;
|
||||
std::unique_ptr<RobloxCallProtocolBridge> call_protocol_bridge_;
|
||||
// Optional: a payload without the AccountProtocol entrypoints still starts.
|
||||
std::unique_ptr<RobloxAccountBridge> account_bridge_;
|
||||
std::shared_ptr<WebViewHelperProcess> web_surface_process_;
|
||||
std::shared_ptr<WebSurfaceExitTarget> web_surface_exit_target_;
|
||||
std::shared_ptr<LifecycleTarget> lifecycle_target_;
|
||||
@@ -257,6 +271,13 @@ class RobloxExperienceComposition final {
|
||||
uint64_t web_surface_logical_generation_ = 0;
|
||||
uint64_t next_web_surface_process_generation_ = 1;
|
||||
uint64_t next_web_surface_logical_generation_ = 1;
|
||||
// The current surface closes as soon as its solved challenge is forwarded.
|
||||
bool web_surface_close_when_challenge_solved_ = false;
|
||||
// Set when a solved challenge is forwarded; applies only to that logical
|
||||
// surface generation.
|
||||
std::optional<std::chrono::steady_clock::time_point>
|
||||
solved_challenge_close_deadline_;
|
||||
uint64_t solved_challenge_logical_generation_ = 0;
|
||||
SecureWebViewRobloxCookie web_view_cookie_;
|
||||
std::string web_view_cookie_initialization_error_;
|
||||
bool web_view_cookie_synchronized_ = false;
|
||||
|
||||
@@ -40,9 +40,10 @@ struct AndroidKeyMapping {
|
||||
// through with an ABI-incompatible numeric value.
|
||||
AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode, uint32_t sdl_key = 0);
|
||||
|
||||
// Mirrors the APK's MotionEvent.getActionButton() - 1 conversion. SDL button
|
||||
// numbers are not numerically compatible with Android button bit values.
|
||||
int32_t MapSdlMouseButtonToAndroid(uint8_t sdl_button);
|
||||
// Translates SDL button numbers to the nativePassMouseButton argument.
|
||||
// The first three are Roblox's zero-based MouseButton1/2/3 indices, not Android
|
||||
// MotionEvent button bits.
|
||||
int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button);
|
||||
|
||||
int32_t MapSdlGamepadButtonToAndroid(uint8_t sdl_button);
|
||||
int32_t RobloxGamepadType(platform::GamepadFamily family);
|
||||
|
||||
@@ -19,6 +19,13 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
|
||||
Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
// Converts a WebView launchGame command
|
||||
// ({"functionName":"launchGame","params":{"request":{...}}}) into the same
|
||||
// contract. Returns kUnsupported for other commands and kInvalidArgument for
|
||||
// a malformed launchGame request.
|
||||
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_MESSAGE_BUS_REQUEST_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_MESSAGE_BUS_REQUEST_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
struct RobloxMessageBusSymbols {
|
||||
jobject (*subscribe_request)(JNIEnv*, jobject, jstring, jstring, jobject,
|
||||
jboolean) = nullptr;
|
||||
void (*delete_connection)(JNIEnv*, jobject, jlong) = nullptr;
|
||||
void (*publish_response)(JNIEnv*, jobject, jstring, jstring, jstring, jint,
|
||||
jstring) = nullptr;
|
||||
void (*set_async_handler)(JNIEnv*, jobject, jstring, jstring,
|
||||
jobject) = nullptr;
|
||||
void (*clear_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
void (*call_response_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return subscribe_request && delete_connection && publish_response &&
|
||||
set_async_handler && clear_handler && call_response_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxMessageBusObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void* context = nullptr;
|
||||
jobject (*create_raw_callback)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring)) = nullptr;
|
||||
void (*clear_raw_callback)(void*, jobject) = nullptr;
|
||||
jobject (*create_async_handler)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_async_handler)(void*, jobject) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus && context && create_raw_callback &&
|
||||
clear_raw_callback && create_async_handler && clear_async_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxMessageBusReply {
|
||||
std::string json;
|
||||
jint code = 0;
|
||||
const char* outcome = "ok";
|
||||
};
|
||||
|
||||
// Owns both legacy subscriptions and correlated async request handlers.
|
||||
// A policy receives only a request's method index and JNI-local payload.
|
||||
class RobloxMessageBusRequestBridge final {
|
||||
public:
|
||||
using Handler =
|
||||
std::function<RobloxMessageBusReply(JNIEnv*, jstring, std::size_t)>;
|
||||
RobloxMessageBusRequestBridge(JniEnvironmentProvider environment,
|
||||
RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects,
|
||||
std::string protocol,
|
||||
std::vector<std::string> methods,
|
||||
Handler handler);
|
||||
~RobloxMessageBusRequestBridge();
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxMessageBusSymbols symbols_;
|
||||
const RobloxMessageBusObjects objects_;
|
||||
const std::string protocol_name_;
|
||||
const std::vector<std::string> methods_;
|
||||
const Handler handler_;
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
#endif
|
||||
@@ -1,58 +1,12 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
#include "runtime/roblox_message_bus_request_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
using RobloxPermissionsMessageBusSymbols = RobloxMessageBusSymbols;
|
||||
using RobloxPermissionsMessageBusObjects = RobloxMessageBusObjects;
|
||||
|
||||
// Exact MessageBus JNI entrypoints used by the APK PermissionsProtocol.
|
||||
struct RobloxPermissionsMessageBusSymbols {
|
||||
jobject (*subscribe_request)(JNIEnv*, jobject, jstring, jstring, jobject,
|
||||
jboolean) = nullptr;
|
||||
void (*delete_connection)(JNIEnv*, jobject, jlong) = nullptr;
|
||||
void (*publish_response)(JNIEnv*, jobject, jstring, jstring, jstring, jint,
|
||||
jstring) = nullptr;
|
||||
void (*set_async_handler)(JNIEnv*, jobject, jstring, jstring,
|
||||
jobject) = nullptr;
|
||||
void (*clear_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
void (*call_response_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return subscribe_request && delete_connection && publish_response &&
|
||||
set_async_handler && clear_handler && call_response_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxPermissionsMessageBusObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void* context = nullptr;
|
||||
jobject (*create_raw_callback)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring)) = nullptr;
|
||||
void (*clear_raw_callback)(void*, jobject) = nullptr;
|
||||
jobject (*create_async_handler)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_async_handler)(void*, jobject) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus && context && create_raw_callback &&
|
||||
clear_raw_callback && create_async_handler && clear_async_handler;
|
||||
}
|
||||
};
|
||||
|
||||
// Installs both legacy protocol subscriptions and correlated async handlers
|
||||
// before native bootstrap. MICROPHONE_ACCESS follows host capture
|
||||
// configuration; LOCAL_NETWORK needs no extra runtime permission, matching the
|
||||
// Android APK. No query/request opens an audio device, enables a disabled
|
||||
// microphone, or changes Roblox account eligibility. Other capabilities remain
|
||||
// unsupported.
|
||||
class RobloxPermissionsBridge final {
|
||||
public:
|
||||
RobloxPermissionsBridge(JniEnvironmentProvider environment,
|
||||
@@ -60,26 +14,12 @@ class RobloxPermissionsBridge final {
|
||||
RobloxPermissionsMessageBusObjects objects,
|
||||
bool microphone_enabled);
|
||||
~RobloxPermissionsBridge();
|
||||
RobloxPermissionsBridge(const RobloxPermissionsBridge&) = delete;
|
||||
RobloxPermissionsBridge& operator=(const RobloxPermissionsBridge&) = delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxPermissionsMessageBusSymbols symbols_;
|
||||
const RobloxPermissionsMessageBusObjects objects_;
|
||||
const bool microphone_enabled_;
|
||||
// Serializes lifecycle only. Native dispatch never holds this mutex.
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::shared_ptr<State> state_;
|
||||
std::unique_ptr<RobloxMessageBusRequestBridge> bridge_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PLATFORM_WEB_SYMBOLS_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PLATFORM_WEB_SYMBOLS_H_
|
||||
|
||||
#include "runtime/roblox_account_bridge.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
@@ -15,6 +16,8 @@ struct RobloxPlatformWebSymbols {
|
||||
RobloxWebViewMessageBusSymbols web_view;
|
||||
RobloxBrowserServiceSymbols browser_service;
|
||||
RobloxPermissionsMessageBusSymbols permissions;
|
||||
// Optional and excluded from complete(); see RobloxExperienceComposition.
|
||||
RobloxAccountProtocolSymbols account;
|
||||
|
||||
bool complete() const {
|
||||
return web_view.complete() && browser_service.complete() &&
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct ResolvedFollowUserPlace {
|
||||
int64_t place_id = 0;
|
||||
std::string game_instance_id; // Roblox's "gameId" for the running session.
|
||||
};
|
||||
|
||||
// Reads the session cookie mocktail already persists at
|
||||
// ~/.local/share/mocktail/auth/roblox.cookie (or $XDG_DATA_HOME
|
||||
// equivalent). Returns an empty string if no session is stored.
|
||||
std::string ReadStoredRoblosecurityCookie();
|
||||
|
||||
// Calls Roblox's presence API to find what place/instance a user is
|
||||
// currently in. `auth_cookie` is the runtime's already-authenticated
|
||||
// .ROBLOSECURITY (or equivalent bearer token) — this function performs no
|
||||
// authentication of its own and requires the runtime's validated session.
|
||||
//
|
||||
// Returns an error if the user is offline, has presence hidden, or is not
|
||||
// in a joinable experience (e.g. in Studio, or privacy settings block it).
|
||||
Status ResolveFollowUserPlace(int64_t user_id, const std::string& auth_cookie,
|
||||
ResolvedFollowUserPlace* out);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
@@ -42,6 +44,10 @@ struct RobloxWebViewOpenRequest {
|
||||
std::optional<bool> show_domain_as_title;
|
||||
std::optional<bool> back_button_visible;
|
||||
std::optional<bool> hide_header;
|
||||
// Set by the bridge, never parsed. The host closes an OPEN_CAPTCHA_VIEW
|
||||
// window once its solved captcha has been forwarded, as Android's captcha
|
||||
// activity finishes itself on success.
|
||||
bool close_when_challenge_solved = false;
|
||||
};
|
||||
|
||||
// WebViewProtocol.mutateWindow is a partial update. Field absence must remain
|
||||
@@ -61,6 +67,9 @@ Status ParseRobloxWebViewMutationJson(const std::string& json,
|
||||
RobloxWebViewMutationRequest* request);
|
||||
Status ParseRobloxCaptchaOpenJson(const std::string& json,
|
||||
RobloxWebViewOpenRequest* request);
|
||||
// True for a page's Navigation.navigateToFeature command reporting a solved
|
||||
// verification (see webview::IsRobloxChallengeSolvedFeature).
|
||||
bool IsRobloxWebViewChallengeSuccess(std::string_view command);
|
||||
Status ParseRobloxProfileViewUrl(const std::string& url,
|
||||
RobloxWebViewOpenRequest* request);
|
||||
Status ParseRobloxPurchaseRobuxNotification(const std::string& json,
|
||||
@@ -108,6 +117,33 @@ using BroadcastWebViewDataModelFocusFn = void (*)(JNIEnv *, jclass, jstring,
|
||||
using SignalWebViewJavascriptCallbackFn = void (*)(JNIEnv*, jclass, jstring);
|
||||
using UpdateRobloxCookieSetHandlerFn =
|
||||
void (*)(JNIEnv*, jobject, jobject);
|
||||
// Roblox's counted handle to its Subsystem<IWebViewProtocol>. The protocol,
|
||||
// and its handleJavascriptCallback subscriber, exist only while a handle is
|
||||
// held. Roblox's handle type is not trivially destructible, so its getter
|
||||
// returns it through the caller-supplied result slot (rdi on x86-64, x8 on
|
||||
// AArch64). The user-provided destructor gives this type the same calling
|
||||
// convention. It does not release the handle.
|
||||
class RobloxWebViewProtocolHandle final {
|
||||
public:
|
||||
RobloxWebViewProtocolHandle() = default;
|
||||
explicit RobloxWebViewProtocolHandle(void* holder) : holder_(holder) {}
|
||||
RobloxWebViewProtocolHandle(const RobloxWebViewProtocolHandle&) = default;
|
||||
RobloxWebViewProtocolHandle& operator=(const RobloxWebViewProtocolHandle&) =
|
||||
default;
|
||||
~RobloxWebViewProtocolHandle() {}
|
||||
|
||||
void* holder() const { return holder_; }
|
||||
|
||||
private:
|
||||
void* holder_ = nullptr;
|
||||
};
|
||||
static_assert(sizeof(RobloxWebViewProtocolHandle) == sizeof(void*));
|
||||
static_assert(!std::is_trivially_destructible_v<RobloxWebViewProtocolHandle>);
|
||||
|
||||
// Roblox's Subsystem<IWebViewProtocol> getter. It is not exported; the
|
||||
// resolver finds it through the first call in
|
||||
// WebViewProtocol.initializeAndroidWebViewProtocol.
|
||||
using AcquireWebViewProtocolFn = RobloxWebViewProtocolHandle (*)();
|
||||
|
||||
struct RobloxWebViewMessageBusSymbols {
|
||||
GetWebViewOpenWindowIdFn get_open_window_id = nullptr;
|
||||
@@ -129,6 +165,8 @@ struct RobloxWebViewMessageBusSymbols {
|
||||
GetWebViewCloseWindowIdFn get_close_window_id = nullptr;
|
||||
SignalWebViewJavascriptCallbackFn signal_javascript_callback = nullptr;
|
||||
UpdateRobloxCookieSetHandlerFn update_cookie_set_handler = nullptr;
|
||||
// Optional: found by code contract, not by symbol name.
|
||||
AcquireWebViewProtocolFn acquire_web_view_protocol = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return get_open_window_id != nullptr &&
|
||||
@@ -287,6 +325,10 @@ private:
|
||||
std::array<std::shared_ptr<RawCallbackTarget>, kSubscriptionCount>
|
||||
raw_callback_targets_{};
|
||||
std::shared_ptr<HostWindowCloseTarget> host_window_close_target_;
|
||||
// The reference this counts is kept for the process lifetime, as Roblox's
|
||||
// Android app start keeps its own. Neither Shutdown nor destruction
|
||||
// releases it.
|
||||
RobloxWebViewProtocolHandle web_view_protocol_handle_;
|
||||
std::size_t in_flight_dispatches_ = 0;
|
||||
bool initializing_ = false;
|
||||
bool shutting_down_ = false;
|
||||
|
||||
@@ -14,7 +14,7 @@ struct ActivePayloadPaths {
|
||||
std::filesystem::path root;
|
||||
std::filesystem::path roblox_library;
|
||||
std::filesystem::path base_apk;
|
||||
std::filesystem::path x86_64_split_apk;
|
||||
std::filesystem::path arch_split_apk;
|
||||
std::filesystem::path assets_content;
|
||||
std::filesystem::path compatibility_manifest;
|
||||
std::filesystem::path host_abi_profile;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -33,11 +34,18 @@ struct PayloadGarbageResult {
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Optional per-store fault hook for atomic manifest writes. Returning true
|
||||
// makes the selected operation fail through its normal cleanup path.
|
||||
enum class ManifestWritePhase { kCreate, kWrite, kFileFsync, kRename };
|
||||
using ManifestWriteFault =
|
||||
std::function<bool(const std::filesystem::path&, ManifestWritePhase)>;
|
||||
|
||||
class PayloadStore final {
|
||||
public:
|
||||
PayloadStore(std::filesystem::path root,
|
||||
std::filesystem::path compatibility_manifest,
|
||||
std::filesystem::path runtime_binary = {});
|
||||
std::filesystem::path runtime_binary = {},
|
||||
ManifestWriteFault manifest_write_fault = {});
|
||||
|
||||
PayloadStoreResult Stage(const std::filesystem::path& prepared_payload);
|
||||
PayloadStoreResult Promote(std::string_view payload_id);
|
||||
@@ -60,6 +68,7 @@ class PayloadStore final {
|
||||
std::filesystem::path root_;
|
||||
std::filesystem::path compatibility_manifest_;
|
||||
std::filesystem::path runtime_binary_;
|
||||
ManifestWriteFault manifest_write_fault_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#ifndef MOCKTAIL_UPDATE_READINESS_CANARY_H_
|
||||
#define MOCKTAIL_UPDATE_READINESS_CANARY_H_
|
||||
|
||||
#include <spawn.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -22,6 +25,12 @@ std::string_view CanaryGraphicsBackendName(CanaryGraphicsBackend backend);
|
||||
bool ParseCanaryGraphicsBackend(std::string_view name,
|
||||
CanaryGraphicsBackend* backend);
|
||||
|
||||
// Optional per-request replacement for the POSIX spawn operation.
|
||||
// It follows posix_spawn return/ownership semantics; empty uses posix_spawn.
|
||||
using CanarySpawn =
|
||||
std::function<int(pid_t*, const char*, const posix_spawn_file_actions_t*,
|
||||
const posix_spawnattr_t*, char* const[], char* const[])>;
|
||||
|
||||
struct CanaryOptions {
|
||||
std::filesystem::path runtime_binary;
|
||||
std::filesystem::path payload_directory;
|
||||
@@ -29,9 +38,9 @@ struct CanaryOptions {
|
||||
std::filesystem::path host_abi_profile;
|
||||
std::filesystem::path cache_root;
|
||||
std::filesystem::path state_root;
|
||||
CanaryGraphicsBackend graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
CanaryGraphicsBackend graphics_backend = CanaryGraphicsBackend::kDirectVulkan;
|
||||
int timeout_seconds = 150;
|
||||
CanarySpawn spawn;
|
||||
|
||||
bool probation_candidate() const { return !host_abi_profile.empty(); }
|
||||
};
|
||||
@@ -39,6 +48,8 @@ struct CanaryOptions {
|
||||
struct CanaryResult {
|
||||
std::filesystem::path log_path;
|
||||
int exit_code = -1;
|
||||
// Direct posix_spawn return code; zero when spawn was not attempted.
|
||||
int spawn_error = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const {
|
||||
|
||||
@@ -26,6 +26,8 @@ struct UpdatePaths {
|
||||
// build the single file happens to describe.
|
||||
std::filesystem::path host_abi_reference_profile;
|
||||
std::filesystem::path runtime_binary;
|
||||
// Optional packaging wrapper; runtime_binary identifies the actual ELF.
|
||||
std::filesystem::path runtime_launcher;
|
||||
};
|
||||
|
||||
struct UpdateRequest {
|
||||
@@ -38,6 +40,7 @@ struct UpdateRequest {
|
||||
CanaryGraphicsBackend canary_graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
int progress_fd = -1;
|
||||
CanarySpawn canary_spawn;
|
||||
};
|
||||
|
||||
struct UpdateResult {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
#define MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::webview {
|
||||
|
||||
// Navigation.navigateToFeature features a Roblox verification page sends when
|
||||
// the user has solved it. The captcha page sends CaptchaSuccess; the generic
|
||||
// challenge page (challenge/cdn/hybrid) sends challengeCompleted. The WebView
|
||||
// helper and the runtime both classify page events with this list.
|
||||
inline bool IsRobloxChallengeSolvedFeature(std::string_view feature) {
|
||||
return feature == "CaptchaSuccess" || feature == "challengeCompleted" ||
|
||||
feature == "ChallengeCompleted";
|
||||
}
|
||||
|
||||
} // namespace mocktail::webview
|
||||
|
||||
#endif // MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
@@ -0,0 +1,10 @@
|
||||
# Allowed reduced aarch64 packages from pkgforge-dev/archlinux-pkgs-debloated.
|
||||
# The installer verifies the rolling release asset IDs and SHA256 digests.
|
||||
ffmpeg-mini-aarch64.pkg.tar.zst
|
||||
gdk-pixbuf2-mini-aarch64.pkg.tar.xz
|
||||
gtk4-mini-aarch64.pkg.tar.zst
|
||||
icu-mini-aarch64.pkg.tar.zst
|
||||
librsvg-mini-aarch64.pkg.tar.zst
|
||||
libxml2-mini-aarch64.pkg.tar.zst
|
||||
mesa-mini-aarch64.pkg.tar.zst
|
||||
opus-mini-aarch64.pkg.tar.zst
|
||||
@@ -7,6 +7,7 @@ MOCKTAIL_PORTABLE_MODE=standalone
|
||||
MOCKTAIL_RUNTIME_LIBRARY_DIR=${SHARUN_DIR}/lib/mocktail
|
||||
MOCKTAIL_BIN=${SHARUN_DIR}/bin/mocktail
|
||||
MOCKTAIL_UPDATE_CANARY_BIN=${SHARUN_DIR}/bin/mocktail
|
||||
MOCKTAIL_UPDATE_RUNTIME_BINARY=${SHARUN_DIR}/shared/bin/mocktail
|
||||
MOCKTAIL_UPDATE_HELPER=${SHARUN_DIR}/bin/mocktail_updater
|
||||
MOCKTAIL_WEBVIEW_HELPER=${SHARUN_DIR}/bin/mocktail_webview_helper
|
||||
MOCKTAIL_FAILURE_DIALOG_HELPER=${SHARUN_DIR}/bin/mocktail_failure_dialog
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--socket=wayland",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=x11",
|
||||
"--socket=pulseaudio",
|
||||
"--device=dri",
|
||||
"--talk-name=com.feralinteractive.GameMode",
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
// cc -O2 -o mocktail_webview_host mocktail_webview_host.c $(pkg-config --cflags --libs gtk4 webkitgtk-6.0)
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <webkit/webkit.h>
|
||||
|
||||
#define PORT 2137
|
||||
#define MAX_PACKET (64u * 1024u + 12u)
|
||||
#define VERSION "2.725.1142"
|
||||
|
||||
enum {
|
||||
OP_LOAD_URL = 1, OP_SET_TITLE = 2, OP_SET_VISIBLE = 3, OP_EVAL_JS = 4,
|
||||
OP_CLOSE = 5, OP_SET_BACK_DISABLED = 6, OP_SHOW_DOMAIN_TITLE = 7,
|
||||
OP_SET_COOKIE = 8, OP_CLEAR_COOKIE = 9, OP_RETAIN_COOKIE = 10
|
||||
};
|
||||
|
||||
enum { EV_EXECUTE_ROBLOX = 1, EV_ROBLOX_WK_HYBRID = 2, EV_READY = 3, EV_COOKIE = 4 };
|
||||
|
||||
static const char kBridge[] =
|
||||
"(() => { 'use strict';"
|
||||
" const h = window.webkit && window.webkit.messageHandlers;"
|
||||
" const handler = h && h.executeRoblox; if (!handler) return;"
|
||||
" const bridge = {};"
|
||||
" Object.defineProperty(bridge, 'executeRoblox', {"
|
||||
" value: (q) => handler.postMessage(JSON.parse(q)),"
|
||||
" enumerable: true, writable: false, configurable: false });"
|
||||
" try { Object.defineProperty(window, '__globalRobloxAndroidBridge__', {"
|
||||
" value: bridge, enumerable: true, writable: false, configurable: false });"
|
||||
" } catch (_) {} })();";
|
||||
|
||||
static GtkApplication *app;
|
||||
static GtkWindow *window;
|
||||
static WebKitWebView *view;
|
||||
static WebKitNetworkSession *session;
|
||||
static int listener = -1;
|
||||
static int client = -1;
|
||||
static guint client_read_source;
|
||||
static guint client_write_source;
|
||||
static GByteArray *rx;
|
||||
static GQueue *tx;
|
||||
static size_t tx_offset;
|
||||
static gboolean domain_title;
|
||||
static gboolean login_page;
|
||||
static gboolean back_disabled;
|
||||
static gboolean shutting_down;
|
||||
static char *last_cookie;
|
||||
|
||||
static gboolean flush_tx(gint fd, GIOCondition cond, gpointer data);
|
||||
static gboolean on_client(gint fd, GIOCondition cond, gpointer data);
|
||||
static void apply(uint8_t op, const char *payload, size_t size);
|
||||
static void create_surface(void);
|
||||
static void destroy_surface(void);
|
||||
|
||||
static void drop_client(const char *why) {
|
||||
if (client < 0) return;
|
||||
fprintf(stderr, "client dropped: %s\n", why);
|
||||
if (client_read_source) {
|
||||
g_source_remove(client_read_source);
|
||||
client_read_source = 0;
|
||||
}
|
||||
if (client_write_source) {
|
||||
g_source_remove(client_write_source);
|
||||
client_write_source = 0;
|
||||
}
|
||||
close(client);
|
||||
client = -1;
|
||||
g_byte_array_set_size(rx, 0);
|
||||
while (!g_queue_is_empty(tx)) g_bytes_unref(g_queue_pop_head(tx));
|
||||
tx_offset = 0;
|
||||
g_free(last_cookie);
|
||||
last_cookie = NULL;
|
||||
destroy_surface();
|
||||
}
|
||||
|
||||
static void quit_app(void) {
|
||||
if (shutting_down) return;
|
||||
shutting_down = TRUE;
|
||||
drop_client("shutting down");
|
||||
if (app) g_application_quit(G_APPLICATION(app));
|
||||
}
|
||||
|
||||
static void send_event(uint8_t type, const char *payload, size_t size) {
|
||||
if (client < 0) return;
|
||||
size_t body = 12 + size;
|
||||
uint8_t *buffer = g_malloc0(4 + body);
|
||||
buffer[0] = body >> 24; buffer[1] = body >> 16; buffer[2] = body >> 8; buffer[3] = body;
|
||||
memcpy(buffer + 4, "MWVE", 4);
|
||||
buffer[8] = 1;
|
||||
buffer[9] = type;
|
||||
buffer[12] = size >> 24; buffer[13] = size >> 16; buffer[14] = size >> 8; buffer[15] = size;
|
||||
if (size) memcpy(buffer + 16, payload, size);
|
||||
g_queue_push_tail(tx, g_bytes_new_take(buffer, 4 + body));
|
||||
if (!client_write_source)
|
||||
client_write_source = g_unix_fd_add(client, G_IO_OUT | G_IO_HUP | G_IO_ERR, flush_tx, NULL);
|
||||
}
|
||||
|
||||
static gboolean flush_tx(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)data;
|
||||
if (cond & (G_IO_HUP | G_IO_ERR)) {
|
||||
client_write_source = 0;
|
||||
drop_client("write side closed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
while (!g_queue_is_empty(tx)) {
|
||||
GBytes *head = g_queue_peek_head(tx);
|
||||
gsize length;
|
||||
const uint8_t *bytes = g_bytes_get_data(head, &length);
|
||||
ssize_t count = send(fd, bytes + tx_offset, length - tx_offset, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return G_SOURCE_CONTINUE;
|
||||
if (count <= 0) {
|
||||
client_write_source = 0;
|
||||
drop_client("write failed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
tx_offset += (size_t)count;
|
||||
if (tx_offset == length) {
|
||||
tx_offset = 0;
|
||||
g_bytes_unref(g_queue_pop_head(tx));
|
||||
}
|
||||
}
|
||||
client_write_source = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static char *user_agent(void) {
|
||||
const char *v = getenv("MOCKTAIL_ROBLOX_VERSION");
|
||||
if (!v || !*v) v = VERSION;
|
||||
return g_strdup_printf(
|
||||
"Mozilla/5.0 AppleWebKit/605.1.15 (KHTML, like Gecko) ROBLOX Android App "
|
||||
"%s Tablet Hybrid() GooglePlayStore RobloxApp/%s (GlobalDist; GooglePlayStore)", v, v);
|
||||
}
|
||||
|
||||
static void on_cookies(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
GList *cookies = webkit_cookie_manager_get_cookies_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
for (GList *i = cookies; i; i = i->next) {
|
||||
SoupCookie *c = i->data;
|
||||
const char *name = soup_cookie_get_name(c);
|
||||
const char *value = soup_cookie_get_value(c);
|
||||
if (name && value && strcmp(name, ".ROBLOSECURITY") == 0 && *value &&
|
||||
(!last_cookie || strcmp(last_cookie, value) != 0)) {
|
||||
g_free(last_cookie);
|
||||
last_cookie = g_strdup(value);
|
||||
send_event(EV_COOKIE, value, strlen(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_list_free_full(cookies, (GDestroyNotify)soup_cookie_free);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void request_cookie(void) {
|
||||
if (!login_page || !session || client < 0) return;
|
||||
webkit_cookie_manager_get_cookies(webkit_network_session_get_cookie_manager(session),
|
||||
"https://www.roblox.com/", NULL, on_cookies, NULL);
|
||||
}
|
||||
|
||||
static void on_cookie_changed(WebKitCookieManager *manager, gpointer data) {
|
||||
(void)manager; (void)data;
|
||||
request_cookie();
|
||||
}
|
||||
|
||||
static void configure_agent(const char *url) {
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(view);
|
||||
GUri *uri = g_uri_parse(url, G_URI_FLAGS_NONE, NULL);
|
||||
const char *path = uri ? g_uri_get_path(uri) : NULL;
|
||||
login_page = path && (strcmp(path, "/login") == 0 || strcmp(path, "/login/") == 0);
|
||||
if (uri) g_uri_unref(uri);
|
||||
if (login_page) {
|
||||
webkit_settings_set_user_agent(settings, NULL);
|
||||
request_cookie();
|
||||
} else {
|
||||
char *agent = user_agent();
|
||||
webkit_settings_set_user_agent(settings, agent);
|
||||
g_free(agent);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_title(void) {
|
||||
if (!domain_title || !window || !view) return;
|
||||
const char *uri = webkit_web_view_get_uri(view);
|
||||
if (!uri) return;
|
||||
GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_NONE, NULL);
|
||||
if (!parsed) return;
|
||||
const char *host = g_uri_get_host(parsed);
|
||||
if (host) gtk_window_set_title(window, host);
|
||||
g_uri_unref(parsed);
|
||||
}
|
||||
|
||||
static void on_load_changed(WebKitWebView *v, WebKitLoadEvent event, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
if (event == WEBKIT_LOAD_COMMITTED || event == WEBKIT_LOAD_FINISHED) {
|
||||
update_title();
|
||||
request_cookie();
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *on_create(WebKitWebView *v, WebKitNavigationAction *action, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
WebKitURIRequest *request = webkit_navigation_action_get_request(action);
|
||||
const char *uri = request ? webkit_uri_request_get_uri(request) : NULL;
|
||||
if (uri && view) webkit_web_view_load_uri(view, uri);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *json_string_after(const char *json, const char *key) {
|
||||
char *needle = g_strdup_printf("\"%s\"", key);
|
||||
const char *at = strstr(json, needle);
|
||||
g_free(needle);
|
||||
if (!at) return NULL;
|
||||
at = strchr(at + strlen(key) + 2, ':');
|
||||
if (!at) return NULL;
|
||||
at++;
|
||||
while (*at == ' ') at++;
|
||||
if (*at != '"') return NULL;
|
||||
at++;
|
||||
GString *out = g_string_new(NULL);
|
||||
while (*at && *at != '"') {
|
||||
if (*at == '\\' && at[1]) at++;
|
||||
g_string_append_c(out, *at);
|
||||
at++;
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
static void on_callback_done(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
gboolean close_after = GPOINTER_TO_INT(data);
|
||||
GError *error = NULL;
|
||||
JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error);
|
||||
gboolean confirmed = !error && value && jsc_value_is_boolean(value) && jsc_value_to_boolean(value);
|
||||
if (value) g_object_unref(value);
|
||||
g_clear_error(&error);
|
||||
if (confirmed && close_after) destroy_surface();
|
||||
}
|
||||
|
||||
static void acknowledge_page_command(const char *json) {
|
||||
if (!view || !json) return;
|
||||
char *feature = json_string_after(json, "feature");
|
||||
char *callback = json_string_after(json, "callbackID");
|
||||
if (feature && callback && *callback) {
|
||||
gboolean success = strcmp(feature, "CaptchaSuccess") == 0;
|
||||
if (success || strcmp(feature, "CaptchaShown") == 0) {
|
||||
GString *quoted = g_string_new("\"");
|
||||
for (const char *p = callback; *p; p++) {
|
||||
if (*p == '"' || *p == '\\') g_string_append_c(quoted, '\\');
|
||||
g_string_append_c(quoted, *p);
|
||||
}
|
||||
g_string_append_c(quoted, '"');
|
||||
char *script = g_strdup_printf(
|
||||
"(() => { try { if (window.Roblox && window.Roblox.Hybrid && "
|
||||
"window.Roblox.Hybrid.Bridge && typeof window.Roblox.Hybrid.Bridge.nativeCallback === 'function') { "
|
||||
"window.Roblox.Hybrid.Bridge.nativeCallback(%s, true, {}); return true; } } catch (_) {} return false; })();",
|
||||
quoted->str);
|
||||
webkit_web_view_evaluate_javascript(view, script, -1, NULL, NULL, NULL,
|
||||
on_callback_done, GINT_TO_POINTER(success));
|
||||
g_free(script);
|
||||
g_string_free(quoted, TRUE);
|
||||
}
|
||||
}
|
||||
g_free(feature);
|
||||
g_free(callback);
|
||||
}
|
||||
|
||||
static void forward(uint8_t type, JSCValue *value) {
|
||||
char *text = NULL;
|
||||
if (jsc_value_is_string(value)) text = jsc_value_to_string(value);
|
||||
else if (jsc_value_is_object(value)) text = jsc_value_to_json(value, 0);
|
||||
if (!text) return;
|
||||
size_t size = strlen(text);
|
||||
if (size && size <= 64u * 1024u) {
|
||||
send_event(type, text, size);
|
||||
acknowledge_page_command(text);
|
||||
}
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
static void on_execute_roblox(WebKitUserContentManager *m, JSCValue *value, gpointer data) {
|
||||
(void)m; (void)data;
|
||||
forward(EV_EXECUTE_ROBLOX, value);
|
||||
}
|
||||
|
||||
static void on_hybrid(WebKitUserContentManager *m, JSCValue *value, gpointer data) {
|
||||
(void)m; (void)data;
|
||||
if (!jsc_value_is_object(value)) return;
|
||||
JSCValue *command = jsc_value_object_get_property(value, "command");
|
||||
if (command) {
|
||||
forward(EV_ROBLOX_WK_HYBRID, command);
|
||||
g_object_unref(command);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_view_close(WebKitWebView *v, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
destroy_surface();
|
||||
}
|
||||
|
||||
static gboolean on_close_request(GtkWindow *w, gpointer data) {
|
||||
(void)w; (void)data;
|
||||
window = NULL;
|
||||
view = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void on_reload(GtkButton *button, gpointer data) {
|
||||
(void)button; (void)data;
|
||||
if (view) webkit_web_view_reload_bypass_cache(view);
|
||||
}
|
||||
|
||||
static void on_eval(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error);
|
||||
if (value) g_object_unref(value);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void on_cookie_done(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
webkit_cookie_manager_add_cookie_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void on_cookie_deleted(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
webkit_cookie_manager_delete_cookie_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void ensure_session(void) {
|
||||
if (session) return;
|
||||
gchar *data_dir = g_build_filename(g_get_user_data_dir(), "mocktail", "webview", NULL);
|
||||
gchar *cache_dir = g_build_filename(g_get_user_cache_dir(), "mocktail", "webview", NULL);
|
||||
g_mkdir_with_parents(data_dir, 0700);
|
||||
g_mkdir_with_parents(cache_dir, 0700);
|
||||
gchar *cookie_db = g_build_filename(data_dir, "cookies.sqlite", NULL);
|
||||
session = webkit_network_session_new(data_dir, cache_dir);
|
||||
g_free(data_dir);
|
||||
g_free(cache_dir);
|
||||
WebKitCookieManager *cookies = webkit_network_session_get_cookie_manager(session);
|
||||
webkit_cookie_manager_set_persistent_storage(cookies, cookie_db, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
webkit_cookie_manager_set_accept_policy(cookies, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
|
||||
g_signal_connect(cookies, "changed", G_CALLBACK(on_cookie_changed), NULL);
|
||||
g_free(cookie_db);
|
||||
webkit_network_session_set_itp_enabled(session, FALSE);
|
||||
webkit_network_session_set_persistent_credential_storage_enabled(session, TRUE);
|
||||
}
|
||||
|
||||
static void create_surface(void) {
|
||||
if (window || view) return;
|
||||
ensure_session();
|
||||
|
||||
WebKitUserContentManager *manager = webkit_user_content_manager_new();
|
||||
WebKitWebView *created = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "network-session", session,
|
||||
"user-content-manager", manager, NULL));
|
||||
g_object_unref(manager);
|
||||
manager = webkit_web_view_get_user_content_manager(created);
|
||||
g_signal_connect(manager, "script-message-received::executeRoblox", G_CALLBACK(on_execute_roblox), NULL);
|
||||
g_signal_connect(manager, "script-message-received::RobloxWKHybrid", G_CALLBACK(on_hybrid), NULL);
|
||||
g_signal_connect(manager, "script-message-received::mocktailRobloxBridge", G_CALLBACK(on_execute_roblox), NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "executeRoblox", NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "RobloxWKHybrid", NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "mocktailRobloxBridge", NULL);
|
||||
WebKitUserScript *script = webkit_user_script_new(kBridge, WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
|
||||
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL);
|
||||
webkit_user_content_manager_add_script(manager, script);
|
||||
webkit_user_script_unref(script);
|
||||
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(created);
|
||||
webkit_settings_set_enable_javascript(settings, TRUE);
|
||||
webkit_settings_set_javascript_can_open_windows_automatically(settings, TRUE);
|
||||
webkit_settings_set_enable_back_forward_navigation_gestures(settings, !back_disabled);
|
||||
char *agent = user_agent();
|
||||
webkit_settings_set_user_agent(settings, agent);
|
||||
g_free(agent);
|
||||
|
||||
g_signal_connect(created, "load-changed", G_CALLBACK(on_load_changed), NULL);
|
||||
g_signal_connect(created, "create", G_CALLBACK(on_create), NULL);
|
||||
g_signal_connect(created, "close", G_CALLBACK(on_view_close), NULL);
|
||||
|
||||
GtkWindow *created_window = GTK_WINDOW(gtk_application_window_new(app));
|
||||
gtk_window_set_title(created_window, "Roblox");
|
||||
gtk_window_set_default_size(created_window, 1100, 760);
|
||||
GtkWidget *header = gtk_header_bar_new();
|
||||
GtkWidget *reload = gtk_button_new_from_icon_name("view-refresh-symbolic");
|
||||
g_signal_connect(reload, "clicked", G_CALLBACK(on_reload), NULL);
|
||||
gtk_header_bar_pack_start(GTK_HEADER_BAR(header), reload);
|
||||
gtk_window_set_titlebar(created_window, header);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(created), TRUE);
|
||||
gtk_window_set_child(created_window, GTK_WIDGET(created));
|
||||
g_signal_connect(created_window, "close-request", G_CALLBACK(on_close_request), NULL);
|
||||
|
||||
view = created;
|
||||
window = created_window;
|
||||
}
|
||||
|
||||
static void destroy_surface(void) {
|
||||
GtkWindow *w = window;
|
||||
window = NULL;
|
||||
view = NULL;
|
||||
login_page = FALSE;
|
||||
domain_title = FALSE;
|
||||
if (w) gtk_window_destroy(w);
|
||||
}
|
||||
|
||||
static void apply(uint8_t op, const char *payload, size_t size) {
|
||||
if (op == OP_CLOSE) {
|
||||
destroy_surface();
|
||||
return;
|
||||
}
|
||||
if (op == OP_SET_BACK_DISABLED) {
|
||||
back_disabled = size == 1 && payload[0];
|
||||
if (view) webkit_settings_set_enable_back_forward_navigation_gestures(
|
||||
webkit_web_view_get_settings(view), !back_disabled);
|
||||
return;
|
||||
}
|
||||
if (op == OP_SHOW_DOMAIN_TITLE) {
|
||||
domain_title = size == 1 && payload[0];
|
||||
update_title();
|
||||
return;
|
||||
}
|
||||
if (op == OP_SET_COOKIE || op == OP_CLEAR_COOKIE) {
|
||||
ensure_session();
|
||||
WebKitCookieManager *cookies = webkit_network_session_get_cookie_manager(session);
|
||||
if (op == OP_SET_COOKIE) {
|
||||
char *value = g_strndup(payload, size);
|
||||
SoupCookie *cookie = soup_cookie_new(".ROBLOSECURITY", value, ".roblox.com", "/", -1);
|
||||
g_free(value);
|
||||
if (cookie) {
|
||||
soup_cookie_set_secure(cookie, TRUE);
|
||||
soup_cookie_set_http_only(cookie, TRUE);
|
||||
soup_cookie_set_same_site_policy(cookie, SOUP_SAME_SITE_POLICY_LAX);
|
||||
webkit_cookie_manager_add_cookie(cookies, cookie, NULL, on_cookie_done, NULL);
|
||||
soup_cookie_free(cookie);
|
||||
}
|
||||
} else {
|
||||
SoupCookie *cookie = soup_cookie_new(".ROBLOSECURITY", "", ".roblox.com", "/", -1);
|
||||
if (cookie) {
|
||||
webkit_cookie_manager_delete_cookie(cookies, cookie, NULL, on_cookie_deleted, NULL);
|
||||
soup_cookie_free(cookie);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op == OP_RETAIN_COOKIE) return;
|
||||
if (op == OP_LOAD_URL) create_surface();
|
||||
if (!window || !view) return;
|
||||
switch (op) {
|
||||
case OP_LOAD_URL: {
|
||||
char *url = g_strndup(payload, size);
|
||||
configure_agent(url);
|
||||
webkit_web_view_load_uri(view, url);
|
||||
gtk_window_present(window);
|
||||
g_free(url);
|
||||
break;
|
||||
}
|
||||
case OP_SET_TITLE: {
|
||||
char *title = g_strndup(payload, size);
|
||||
gtk_window_set_title(window, title);
|
||||
g_free(title);
|
||||
break;
|
||||
}
|
||||
case OP_SET_VISIBLE:
|
||||
if (size == 1 && payload[0]) gtk_window_present(window);
|
||||
else gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
|
||||
break;
|
||||
case OP_EVAL_JS:
|
||||
webkit_web_view_evaluate_javascript(view, payload, (gssize)size, NULL, NULL, NULL, on_eval, NULL);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean process_frames(void) {
|
||||
while (rx->len >= 4) {
|
||||
uint32_t length = ((uint32_t)rx->data[0] << 24) | ((uint32_t)rx->data[1] << 16) |
|
||||
((uint32_t)rx->data[2] << 8) | rx->data[3];
|
||||
if (length > MAX_PACKET) return FALSE;
|
||||
if (rx->len < 4u + length) break;
|
||||
uint8_t *copy = g_memdup2(rx->data + 4, length);
|
||||
g_byte_array_remove_range(rx, 0, 4 + length);
|
||||
if (length >= 12 && memcmp(copy, "MWVC", 4) == 0) {
|
||||
size_t size = ((size_t)copy[8] << 24) | ((size_t)copy[9] << 16) |
|
||||
((size_t)copy[10] << 8) | copy[11];
|
||||
if (size == length - 12) apply(copy[5], (const char *)copy + 12, size);
|
||||
}
|
||||
g_free(copy);
|
||||
if (client < 0) return TRUE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean on_client(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)data;
|
||||
uint8_t buffer[8192];
|
||||
while (TRUE) {
|
||||
ssize_t count = read(fd, buffer, sizeof buffer);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
|
||||
if (count <= 0) {
|
||||
client_read_source = 0;
|
||||
drop_client("client closed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
g_byte_array_append(rx, buffer, (guint)count);
|
||||
if (!process_frames()) {
|
||||
client_read_source = 0;
|
||||
drop_client("bad frame");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (client < 0) return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (cond & (G_IO_HUP | G_IO_ERR)) {
|
||||
client_read_source = 0;
|
||||
drop_client("client hung up");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static gboolean on_listener(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)cond; (void)data;
|
||||
int accepted = accept(fd, NULL, NULL);
|
||||
if (accepted < 0) return G_SOURCE_CONTINUE;
|
||||
if (client >= 0) drop_client("replaced by a new connection");
|
||||
fcntl(accepted, F_SETFD, FD_CLOEXEC);
|
||||
fcntl(accepted, F_SETFL, fcntl(accepted, F_GETFL) | O_NONBLOCK);
|
||||
int one = 1;
|
||||
setsockopt(accepted, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
||||
client = accepted;
|
||||
fprintf(stderr, "client connected\n");
|
||||
client_read_source = g_unix_fd_add(client, G_IO_IN | G_IO_HUP | G_IO_ERR, on_client, NULL);
|
||||
send_event(EV_READY, "", 0);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static int listen_socket(void) {
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
fcntl(fd, F_SETFD, FD_CLOEXEC);
|
||||
int one = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||
struct sockaddr_in address;
|
||||
memset(&address, 0, sizeof address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(PORT);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof address) != 0 || listen(fd, 4) != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void activate(GtkApplication *application, gpointer data) {
|
||||
(void)application; (void)data;
|
||||
}
|
||||
|
||||
static gboolean on_sigterm(gpointer data) {
|
||||
(void)data;
|
||||
quit_app();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", 0);
|
||||
setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", 0);
|
||||
rx = g_byte_array_new();
|
||||
tx = g_queue_new();
|
||||
listener = listen_socket();
|
||||
if (listener < 0) {
|
||||
fprintf(stderr, "cannot listen on 127.0.0.1:%d: %s\n", PORT, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
g_unix_fd_add(listener, G_IO_IN, on_listener, NULL);
|
||||
g_unix_signal_add(SIGTERM, on_sigterm, NULL);
|
||||
g_unix_signal_add(SIGINT, on_sigterm, NULL);
|
||||
app = gtk_application_new("org.mocktail.WebViewHost", G_APPLICATION_NON_UNIQUE);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||
g_application_hold(G_APPLICATION(app));
|
||||
int status = g_application_run(G_APPLICATION(app), 1, argv);
|
||||
g_object_unref(app);
|
||||
return status;
|
||||
}
|
||||
@@ -57,6 +57,32 @@
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="0.3.3" date="2026-10-04">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Joining a friend from a follow user link now works.</li>
|
||||
<li>Roblox textures decode about 3 times faster, so loading stalls are shorter.</li>
|
||||
<li>A failed Roblox update now restores your previous install instead of leaving it half changed.</li>
|
||||
<li>Fixed crashes and freezes with audio, the text overlay, the mouse lock and saved themes.</li>
|
||||
<li>Going to another page in the web view no longer closes it.</li>
|
||||
<li>Clicking while typing no longer closes the text box.</li>
|
||||
<li>Fixed the sky showing the wrong textures.</li>
|
||||
<li>The Flatpak build on NVIDIA now uses X11 to avoid Wayland hangs.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.2" date="2026-09-29">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added experimental support for ARM64 Linux.</li>
|
||||
<li>Fixed login stalling after passing a verification check.</li>
|
||||
<li>Fixed login check windows staying open after they were solved.</li>
|
||||
<li>Fixed Play on a private server doing nothing until a sidebar tab was clicked.</li>
|
||||
<li>Less stutter while joining a game and while textures load in.</li>
|
||||
<li>Middle click and natural scrolling now work as expected.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.1" date="2026-09-20">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -461,6 +461,7 @@ LatestSupportedBootstrapProfile() {
|
||||
.version_code <= ($remote_version_code | tonumber)) and
|
||||
.status == "supported" and .default_allowed == true and
|
||||
.allow_legacy_binary_patches == false and
|
||||
(.abi // "x86_64") == "x86_64" and
|
||||
(.version_name | type) == "string" and
|
||||
(.version_name | length) > 0)] |
|
||||
group_by(.version_code) |
|
||||
@@ -495,6 +496,7 @@ SelectSupportedBootstrapFallback() {
|
||||
bootstrap_source="$(jq -er --argjson version_code "${fallback_version_code}" '
|
||||
[.sources[]? |
|
||||
select(.version_code == $version_code and
|
||||
(.abi // "x86_64") == "x86_64" and
|
||||
.provider == "uptodown")] |
|
||||
if length == 1 then .[0].provider else empty end
|
||||
' "${BOOTSTRAP_SOURCES_PATH}" 2>/dev/null || true)"
|
||||
|
||||
@@ -329,6 +329,7 @@ def run_trial(args, max_ctors, skipped_offsets, trial_id):
|
||||
log_path = args.log_dir / f"trial_{trial_id:03d}_max_{max_ctors}.log"
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"ROBLOX_LIB_PATH": str(args.libroblox),
|
||||
"MOCKTAIL_RUN_LIBROBLOX_CTORS": "1",
|
||||
"MOCKTAIL_LIBROBLOX_CTOR_POLICY": args.ctor_policy,
|
||||
"MOCKTAIL_GRAPHICS_BACKEND": args.graphics_backend,
|
||||
@@ -500,7 +501,7 @@ def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--project-root", type=Path, default=project_root)
|
||||
parser.add_argument("--run-script", type=Path,
|
||||
default=project_root / "scripts" / "run_sober.sh")
|
||||
default=project_root / "build" / "mocktail")
|
||||
parser.add_argument("--libroblox", type=Path,
|
||||
default=project_root / "rbx_bin" / "libroblox.so")
|
||||
parser.add_argument("--log-dir", type=Path,
|
||||
|
||||
@@ -10,18 +10,18 @@ readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
||||
|
||||
BUILD_DIR="${PROJECT_ROOT}/build-flatpak"
|
||||
MANIFEST="${PROJECT_ROOT}/packaging/flatpak/io.github.CoderDayton.nightcap.json"
|
||||
JOBS="${MOCKTAIL_FLATPAK_JOBS:-4}"
|
||||
JOBS="${MOCKTAIL_FLATPAK_JOBS:-2}"
|
||||
|
||||
Usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/build_flatpak.sh [OPTIONS]
|
||||
|
||||
Build and install Mocktail as a per-user x86-64 Flatpak.
|
||||
Build and install Mocktail as a per-user Flatpak for the host architecture.
|
||||
|
||||
Options:
|
||||
--build-dir DIR Builder output directory (default: build-flatpak).
|
||||
--manifest FILE Flatpak manifest path.
|
||||
--jobs N Maximum parallel build jobs (default: 4).
|
||||
--jobs N Maximum parallel build jobs (default: 2).
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
@@ -93,8 +93,11 @@ done
|
||||
[[ "${JOBS}" =~ ^[1-9][0-9]*$ && "${JOBS}" -le 256 ]] ||
|
||||
Die "--jobs must be an integer between 1 and 256"
|
||||
|
||||
[[ "$(uname -m)" == x86_64 ]] ||
|
||||
Die "the Roblox payload and Flatpak manifest currently require x86-64"
|
||||
HOST_ARCH="$(uname -m)"
|
||||
case "${HOST_ARCH}" in
|
||||
x86_64|aarch64) ;;
|
||||
*) Die "Flatpak builds require an x86_64 or aarch64 Linux host" ;;
|
||||
esac
|
||||
command -v flatpak >/dev/null 2>&1 || Die "flatpak is required"
|
||||
|
||||
MANIFEST="$(realpath -e -- "${MANIFEST}")" ||
|
||||
@@ -123,7 +126,7 @@ CleanupStaleBuilderMounts
|
||||
|
||||
cd -- "${PROJECT_ROOT}"
|
||||
exec "${builder[@]}" \
|
||||
--arch=x86_64 \
|
||||
--arch="${HOST_ARCH}" \
|
||||
--jobs="${JOBS}" \
|
||||
--force-clean \
|
||||
--install-deps-from=flathub \
|
||||
|
||||
@@ -61,6 +61,7 @@ def supported_profiles(catalog: dict) -> list[dict]:
|
||||
if isinstance(profile, dict)
|
||||
and profile.get("status") == "supported"
|
||||
and profile.get("default_allowed") is True
|
||||
and profile.get("abi", ARCHITECTURE) == ARCHITECTURE
|
||||
and isinstance(profile.get("version_name"), str)
|
||||
and isinstance(profile.get("version_code"), int)
|
||||
]
|
||||
|
||||
@@ -18,12 +18,15 @@ AnyLinuxDependencyRequireContainer() {
|
||||
}
|
||||
|
||||
AnyLinuxDependencySnapshot() {
|
||||
local manifest="$1" release="$2" filename remainder names_json
|
||||
local manifest="$1" release="$2" host_arch="${3:-$(uname -m)}"
|
||||
local filename remainder names_json
|
||||
[[ "${host_arch}" == x86_64 || "${host_arch}" == aarch64 ]] ||
|
||||
AnyLinuxDependencyDie 'unsupported AppImage architecture'
|
||||
local -a names=()
|
||||
local -A seen=()
|
||||
while read -r filename remainder || [[ -n "${filename}" ]]; do
|
||||
[[ -n "${filename}" && "${filename}" != \#* ]] || continue
|
||||
[[ "${filename}" =~ ^[a-z0-9-]+-mini-x86_64\.pkg\.tar\.zst$ &&
|
||||
[[ "${filename}" =~ ^[a-z0-9-]+-mini-${host_arch}\.pkg\.tar\.(zst|xz)$ &&
|
||||
-z "${remainder}" && -z "${seen[${filename}]:-}" ]] ||
|
||||
AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry'
|
||||
seen["${filename}"]=1
|
||||
@@ -108,7 +111,13 @@ AnyLinuxDependenciesMain() (
|
||||
AnyLinuxDependencyRequireContainer
|
||||
(( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported'
|
||||
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.txt"
|
||||
local host_arch="$(uname -m)" manifest_name
|
||||
case "${host_arch}" in
|
||||
x86_64) manifest_name=anylinux-dependencies.txt ;;
|
||||
aarch64) manifest_name=anylinux-dependencies-aarch64.txt ;;
|
||||
*) AnyLinuxDependencyDie "unsupported AppImage architecture: ${host_arch}" ;;
|
||||
esac
|
||||
readonly MANIFEST="${ROOT}/packaging/${manifest_name}"
|
||||
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
|
||||
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
|
||||
AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}"
|
||||
|
||||
@@ -409,9 +409,11 @@ WriteClassicPixbufCache() {
|
||||
Die "classic GdkPixbuf loader cache is unavailable"
|
||||
sed -E \
|
||||
-e 's@^# LoaderDir = .*$@# LoaderDir = lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders@' \
|
||||
-e 's#^"[^"]*/(libpixbufloader[-_][^/"]+\.so)"$#"lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders/\1"#' \
|
||||
-e 's#^"[^"]*/([^/"]+\.so)"$#"lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders/\1"#' \
|
||||
"${source_cache}" >"${destination_cache}"
|
||||
if grep -Eq '^"/(usr|lib|lib64|opt|home|hdd|tmp)/' \
|
||||
# Module records contain only a quoted path. Image signatures such as XPM
|
||||
# can also start with '/' but have additional fields after the string.
|
||||
if grep -Eq '^"/[^"]*"$' \
|
||||
"${destination_cache}"; then
|
||||
Die "GdkPixbuf cache still contains an absolute loader path"
|
||||
fi
|
||||
@@ -663,4 +665,6 @@ Main() {
|
||||
PublishResources
|
||||
}
|
||||
|
||||
Main "$@"
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
Main "$@"
|
||||
fi
|
||||
@@ -28,6 +28,7 @@ STORE_ROOT=""
|
||||
TEMP_DIR=""
|
||||
ROLLBACK_DIR=""
|
||||
ROLLBACK_NEEDED=false
|
||||
BACKUP_COMPLETE=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -146,34 +147,54 @@ certificate_digests() {
|
||||
}
|
||||
|
||||
restore_payload() {
|
||||
[[ "${ROLLBACK_NEEDED}" == true && -d "${ROLLBACK_DIR}" ]] || return 0
|
||||
[[ "${ROLLBACK_NEEDED}" == true ]] || return 0
|
||||
[[ -d "${ROLLBACK_DIR}" ]] || return 1
|
||||
|
||||
log WARN "restoring the previous payload"
|
||||
mkdir -p "${RBX_BIN_DIR}" "$(dirname -- "${METADATA_PATH}")"
|
||||
mkdir -p "${RBX_BIN_DIR}" "$(dirname -- "${METADATA_PATH}")" || return 1
|
||||
|
||||
local path
|
||||
for path in libroblox.so sober_apk assets; do
|
||||
if [[ -e "${RBX_BIN_DIR}/${path}" ]]; then
|
||||
mv -- "${RBX_BIN_DIR}/${path}" "${TEMP_DIR}/failed-${path}"
|
||||
local path active_path failed_path restore_status=0
|
||||
for path in libroblox.so sober_apk assets roblox_payload.json; do
|
||||
active_path="${RBX_BIN_DIR}/${path}"
|
||||
failed_path="${TEMP_DIR}/failed-${path}"
|
||||
if [[ "${path}" == roblox_payload.json ]]; then
|
||||
active_path="${METADATA_PATH}"
|
||||
failed_path="${TEMP_DIR}/failed-metadata.json"
|
||||
fi
|
||||
if [[ -e "${ROLLBACK_DIR}/${path}" ]]; then
|
||||
mv -- "${ROLLBACK_DIR}/${path}" "${RBX_BIN_DIR}/${path}"
|
||||
|
||||
# Before activation, anything still active belongs to the old payload.
|
||||
# Only components removed by the backup need restoring in this phase.
|
||||
if [[ "${BACKUP_COMPLETE}" == false ]]; then
|
||||
if [[ -e "${active_path}" || -L "${active_path}" ||
|
||||
! -e "${ROLLBACK_DIR}/${path}" && ! -L "${ROLLBACK_DIR}/${path}" ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e "${active_path}" || -L "${active_path}" ]]; then
|
||||
# Do not restore onto an occupied destination if removing the new
|
||||
# component fails: mv could overwrite a file or nest a directory.
|
||||
if ! mv -- "${active_path}" "${failed_path}"; then
|
||||
restore_status=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [[ -e "${ROLLBACK_DIR}/${path}" || -L "${ROLLBACK_DIR}/${path}" ]]; then
|
||||
mv -- "${ROLLBACK_DIR}/${path}" "${active_path}" || restore_status=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -e "${METADATA_PATH}" ]]; then
|
||||
mv -- "${METADATA_PATH}" "${TEMP_DIR}/failed-metadata.json"
|
||||
fi
|
||||
if [[ -e "${ROLLBACK_DIR}/roblox_payload.json" ]]; then
|
||||
mv -- "${ROLLBACK_DIR}/roblox_payload.json" "${METADATA_PATH}"
|
||||
fi
|
||||
return "${restore_status}"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local -r status=$?
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
set +e
|
||||
restore_payload
|
||||
if ! restore_payload; then
|
||||
log ERROR "payload restoration failed; recovery files retained at ${TEMP_DIR}"
|
||||
(( status != 0 )) || status=1
|
||||
exit "${status}"
|
||||
fi
|
||||
if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then
|
||||
rm -rf -- "${TEMP_DIR}"
|
||||
fi
|
||||
@@ -401,17 +422,18 @@ main() {
|
||||
mkdir -p "${VERSIONS_DIR}" "$(dirname -- "${METADATA_PATH}")"
|
||||
ROLLBACK_DIR="${TEMP_DIR}/rollback"
|
||||
mkdir -p "${ROLLBACK_DIR}"
|
||||
ROLLBACK_NEEDED=true
|
||||
|
||||
local active_path
|
||||
for active_path in libroblox.so sober_apk assets; do
|
||||
if [[ -e "${RBX_BIN_DIR}/${active_path}" ]]; then
|
||||
if [[ -e "${RBX_BIN_DIR}/${active_path}" || -L "${RBX_BIN_DIR}/${active_path}" ]]; then
|
||||
mv -- "${RBX_BIN_DIR}/${active_path}" "${ROLLBACK_DIR}/${active_path}"
|
||||
fi
|
||||
done
|
||||
if [[ -e "${METADATA_PATH}" ]]; then
|
||||
if [[ -e "${METADATA_PATH}" || -L "${METADATA_PATH}" ]]; then
|
||||
mv -- "${METADATA_PATH}" "${ROLLBACK_DIR}/roblox_payload.json"
|
||||
fi
|
||||
ROLLBACK_NEEDED=true
|
||||
BACKUP_COMPLETE=true
|
||||
|
||||
mv -- "${stage_dir}/libroblox.so" "${RBX_BIN_DIR}/libroblox.so"
|
||||
mv -- "${stage_dir}/sober_apk" "${RBX_BIN_DIR}/sober_apk"
|
||||
|
||||
@@ -110,8 +110,14 @@ for elf in "$@"; do
|
||||
fi
|
||||
machine="$(LC_ALL=C readelf --file-header "${elf}" 2>/dev/null |
|
||||
sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')"
|
||||
if [[ "${machine}" != *X86-64* ]]; then
|
||||
printf 'ABI check: %s is not x86-64 (%s)\n' "${elf}" "${machine}" >&2
|
||||
case "$(uname -m)" in
|
||||
x86_64) expected_machine='X86-64' ;;
|
||||
aarch64) expected_machine='AArch64' ;;
|
||||
*) expected_machine='X86-64' ;;
|
||||
esac
|
||||
if [[ "${machine}" != *"${expected_machine}"* ]]; then
|
||||
printf 'ABI check: %s is not %s (%s)\n' "${elf}" "${expected_machine}" \
|
||||
"${machine}" >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::audio {
|
||||
namespace {
|
||||
@@ -55,10 +53,8 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
};
|
||||
|
||||
struct ReleaseTicket {
|
||||
std::shared_ptr<State> state;
|
||||
std::uint64_t id = 0;
|
||||
std::uint64_t generation = 0;
|
||||
bool in_use = false;
|
||||
const std::shared_ptr<State> state;
|
||||
const std::uint64_t generation;
|
||||
};
|
||||
|
||||
struct CallbackJob {
|
||||
@@ -71,13 +67,7 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
: sink(std::move(owned_sink)),
|
||||
max_buffers(options.max_buffers),
|
||||
event_callback(options.event_callback),
|
||||
event_context(options.event_context),
|
||||
ticket_pool(options.max_buffers) {
|
||||
free_tickets.reserve(options.max_buffers);
|
||||
for (std::size_t i = options.max_buffers; i > 0; --i) {
|
||||
free_tickets.push_back(i - 1);
|
||||
}
|
||||
}
|
||||
event_context(options.event_context) {}
|
||||
|
||||
static const AndroidSimpleBufferQueueTable kQueueTable;
|
||||
|
||||
@@ -103,7 +93,6 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
const std::shared_ptr<State> state = raw_state->shared_from_this();
|
||||
|
||||
std::lock_guard<std::mutex> operation_lock(state->sink_operation_mutex);
|
||||
std::uint64_t id = 0;
|
||||
ReleaseTicket* ticket = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
@@ -111,24 +100,24 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
return opensl_abi::kResultPreconditionsViolated;
|
||||
}
|
||||
if (state->pending_count.load(std::memory_order_relaxed) >=
|
||||
state->max_buffers ||
|
||||
state->free_tickets.empty()) {
|
||||
state->max_buffers) {
|
||||
return opensl_abi::kResultBufferInsufficient;
|
||||
}
|
||||
const std::size_t slot_index = state->free_tickets.back();
|
||||
state->free_tickets.pop_back();
|
||||
ticket = &state->ticket_pool[slot_index];
|
||||
id = state->next_id++;
|
||||
ticket->state = state;
|
||||
ticket->id = id;
|
||||
ticket->generation = state->generation;
|
||||
ticket->in_use = true;
|
||||
ticket = new (std::nothrow)
|
||||
ReleaseTicket{state, state->generation};
|
||||
if (ticket == nullptr) {
|
||||
state->last_status = Status::Error(
|
||||
StatusCode::kUnavailable, "unable to allocate OpenSL release ticket");
|
||||
return ToOpenSlResult(state->last_status);
|
||||
}
|
||||
state->pending_count.fetch_add(1, std::memory_order_relaxed);
|
||||
++state->submitted_buffers;
|
||||
}
|
||||
|
||||
const PcmBuffer pcm{buffer, static_cast<std::size_t>(size),
|
||||
&State::ReleaseBuffer, ticket};
|
||||
// A successful enqueue may release synchronously. Only a failed enqueue
|
||||
// leaves ownership here; its contract forbids a release callback.
|
||||
const Status status = state->sink->Enqueue(pcm);
|
||||
if (status.ok()) {
|
||||
state->NotifyEvent(OpenSlBufferQueueEvent::kSubmitted,
|
||||
@@ -136,22 +125,12 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
return opensl_abi::kResultSuccess;
|
||||
}
|
||||
|
||||
const std::unique_ptr<ReleaseTicket> failed_ticket(ticket);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->last_status = status;
|
||||
if (ticket->in_use && ticket->id == id) {
|
||||
ticket->in_use = false;
|
||||
ticket->state.reset();
|
||||
const std::size_t slot_index =
|
||||
static_cast<std::size_t>(ticket - state->ticket_pool.data());
|
||||
state->free_tickets.push_back(slot_index);
|
||||
if (state->pending_count.load(std::memory_order_relaxed) > 0) {
|
||||
state->pending_count.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (state->submitted_buffers > 0) {
|
||||
--state->submitted_buffers;
|
||||
}
|
||||
}
|
||||
state->pending_count.fetch_sub(1, std::memory_order_relaxed);
|
||||
--state->submitted_buffers;
|
||||
}
|
||||
return ToOpenSlResult(status);
|
||||
}
|
||||
@@ -164,25 +143,14 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
}
|
||||
const std::shared_ptr<State> state = raw_state->shared_from_this();
|
||||
std::lock_guard<std::mutex> operation_lock(state->sink_operation_mutex);
|
||||
std::size_t discarded_count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (state->stopping) {
|
||||
return opensl_abi::kResultPreconditionsViolated;
|
||||
}
|
||||
++state->generation;
|
||||
state->free_tickets.clear();
|
||||
for (auto& ticket : state->ticket_pool) {
|
||||
if (ticket.in_use) {
|
||||
ticket.in_use = false;
|
||||
ticket.state.reset();
|
||||
++discarded_count;
|
||||
}
|
||||
}
|
||||
for (std::size_t i = state->max_buffers; i > 0; --i) {
|
||||
state->free_tickets.push_back(i - 1);
|
||||
}
|
||||
state->discarded_buffers += discarded_count;
|
||||
state->discarded_buffers +=
|
||||
state->pending_count.load(std::memory_order_relaxed);
|
||||
state->pending_count.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
const Status status = state->sink->Clear();
|
||||
@@ -226,7 +194,8 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
|
||||
static void ReleaseBuffer(void* context, const void* /*data*/,
|
||||
std::size_t size_bytes) {
|
||||
auto* ticket = static_cast<ReleaseTicket*>(context);
|
||||
const std::unique_ptr<ReleaseTicket> ticket(
|
||||
static_cast<ReleaseTicket*>(context));
|
||||
if (ticket == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -239,29 +208,27 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
bool discarded = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (ticket->in_use) {
|
||||
ticket->in_use = false;
|
||||
ticket->state.reset();
|
||||
const std::size_t slot_index =
|
||||
static_cast<std::size_t>(ticket - state->ticket_pool.data());
|
||||
state->free_tickets.push_back(slot_index);
|
||||
if (state->pending_count.load(std::memory_order_relaxed) > 0) {
|
||||
state->pending_count.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (!state->stopping && ticket->generation == state->generation) {
|
||||
state->processed_buffers.fetch_add(1, std::memory_order_relaxed);
|
||||
++state->consumed_buffers;
|
||||
state->consumed_bytes += size_bytes;
|
||||
consumed = true;
|
||||
if (state->callback != nullptr) {
|
||||
state->callback_jobs.push_back(
|
||||
CallbackJob{state->callback, state->callback_context});
|
||||
notify_worker = true;
|
||||
}
|
||||
} else {
|
||||
++state->discarded_buffers;
|
||||
discarded = true;
|
||||
// Clear already counted older generations as discarded. Their tickets
|
||||
// remain alive until release without affecting newly enqueued buffers.
|
||||
if (ticket->generation != state->generation) {
|
||||
return;
|
||||
}
|
||||
if (state->pending_count.load(std::memory_order_relaxed) > 0) {
|
||||
state->pending_count.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (!state->stopping) {
|
||||
state->processed_buffers.fetch_add(1, std::memory_order_relaxed);
|
||||
++state->consumed_buffers;
|
||||
state->consumed_bytes += size_bytes;
|
||||
consumed = true;
|
||||
if (state->callback != nullptr) {
|
||||
state->callback_jobs.push_back(
|
||||
CallbackJob{state->callback, state->callback_context});
|
||||
notify_worker = true;
|
||||
}
|
||||
} else {
|
||||
++state->discarded_buffers;
|
||||
discarded = true;
|
||||
}
|
||||
}
|
||||
if (consumed) {
|
||||
@@ -374,14 +341,11 @@ struct OpenSlSimpleBufferQueueAdapter::State
|
||||
void* const event_context;
|
||||
Handle handle;
|
||||
pthread_t callback_thread{};
|
||||
std::vector<ReleaseTicket> ticket_pool;
|
||||
std::vector<std::size_t> free_tickets;
|
||||
std::atomic<std::size_t> pending_count{0};
|
||||
std::deque<CallbackJob> callback_jobs;
|
||||
AndroidSimpleBufferQueueCallback callback = nullptr;
|
||||
void* callback_context = nullptr;
|
||||
Status last_status;
|
||||
std::uint64_t next_id = 1;
|
||||
std::uint64_t generation = 1;
|
||||
std::atomic<std::uint64_t> processed_buffers{0};
|
||||
std::uint64_t submitted_buffers = 0;
|
||||
@@ -547,7 +511,6 @@ void OpenSlSimpleBufferQueueAdapter::Shutdown() {
|
||||
}
|
||||
state->shutdown_started = true;
|
||||
state->stopping = true;
|
||||
++state->generation;
|
||||
state->callback = nullptr;
|
||||
state->callback_context = nullptr;
|
||||
state->callback_jobs.clear();
|
||||
@@ -566,10 +529,6 @@ void OpenSlSimpleBufferQueueAdapter::Shutdown() {
|
||||
state->last_status = Status::Error(
|
||||
StatusCode::kPlatformError,
|
||||
"audio sink did not release all borrowed OpenSL buffers");
|
||||
for (auto& ticket : state->ticket_pool) {
|
||||
ticket.in_use = false;
|
||||
ticket.state.reset();
|
||||
}
|
||||
state->pending_count.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
state->shutdown_complete = true;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "compat/bionic_pthread_create_runtime.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "compat/http_client_spin_guard.h"
|
||||
#include "window/window.h"
|
||||
|
||||
@@ -31,6 +33,14 @@ const char* EnvironmentOr(const char* name, const char* fallback) {
|
||||
return value != nullptr && value[0] != '\0' ? value : fallback;
|
||||
}
|
||||
|
||||
const char* GuestAbiPropertyValue() {
|
||||
#if defined(__aarch64__)
|
||||
return "arm64-v8a";
|
||||
#else
|
||||
return "x86_64";
|
||||
#endif
|
||||
}
|
||||
|
||||
int CallHostVsnprintf(char* dst, size_t count, const char* format,
|
||||
va_list args) {
|
||||
using HostVsnprintf = int (*)(char*, size_t, const char*, va_list);
|
||||
@@ -173,8 +183,12 @@ int __system_property_get(const char* name, char* value) {
|
||||
result = "35";
|
||||
} else if (std::strcmp(name, "ro.build.version.release") == 0) {
|
||||
result = "15";
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0) {
|
||||
result = "x86_64";
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abi") == 0 ||
|
||||
std::strcmp(name, "ro.product.cpu.abilist") == 0 ||
|
||||
std::strcmp(name, "ro.product.cpu.abilist64") == 0) {
|
||||
result = GuestAbiPropertyValue();
|
||||
} else if (std::strcmp(name, "ro.product.cpu.abilist32") == 0) {
|
||||
result = "";
|
||||
} else if (std::strcmp(name, "ro.product.manufacturer") == 0) {
|
||||
result = EnvironmentOr("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
|
||||
} else if (std::strcmp(name, "ro.product.model") == 0) {
|
||||
|
||||
@@ -15,6 +15,49 @@ namespace {
|
||||
|
||||
constexpr int kBionicAddressInfoFailure = 4;
|
||||
constexpr int kBionicAddressInfoMemory = 6;
|
||||
constexpr int kBionicAddressInfoBadFlags = 3;
|
||||
|
||||
struct AddressInfoFlag {
|
||||
int bionic;
|
||||
int host;
|
||||
};
|
||||
|
||||
// Bundled Android netdb.h AI_MASK includes only these five flags.
|
||||
constexpr std::array<AddressInfoFlag, 5> kAddressInfoFlags = {{
|
||||
{0x00000001, AI_PASSIVE},
|
||||
{0x00000002, AI_CANONNAME},
|
||||
{0x00000004, AI_NUMERICHOST},
|
||||
{0x00000008, AI_NUMERICSERV},
|
||||
{0x00000400, AI_ADDRCONFIG},
|
||||
}};
|
||||
|
||||
constexpr int SupportedBionicAddressInfoFlags() noexcept {
|
||||
int flags = 0;
|
||||
for (const AddressInfoFlag& entry : kAddressInfoFlags) {
|
||||
flags |= entry.bionic;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
int HostAddressInfoFlags(int bionic_flags) noexcept {
|
||||
int flags = 0;
|
||||
for (const AddressInfoFlag& entry : kAddressInfoFlags) {
|
||||
if ((bionic_flags & entry.bionic) != 0) {
|
||||
flags |= entry.host;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
int BionicAddressInfoFlags(int host_flags) noexcept {
|
||||
int flags = 0;
|
||||
for (const AddressInfoFlag& entry : kAddressInfoFlags) {
|
||||
if ((host_flags & entry.host) != 0) {
|
||||
flags |= entry.bionic;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
constexpr std::array<std::string_view, 3> kBlockedUploadSuffixes = {
|
||||
"crashes.rbxinfra.com",
|
||||
@@ -63,7 +106,7 @@ int BionicAddressInfoError(int host_error) noexcept {
|
||||
return 2;
|
||||
}
|
||||
if (host_error == EAI_BADFLAGS) {
|
||||
return 3;
|
||||
return kBionicAddressInfoBadFlags;
|
||||
}
|
||||
if (host_error == EAI_FAIL) {
|
||||
return kBionicAddressInfoFailure;
|
||||
@@ -102,7 +145,7 @@ int BionicAddressInfoError(int host_error) noexcept {
|
||||
addrinfo HostHints(const BionicAddressInfo* hints) noexcept {
|
||||
addrinfo host_hints{};
|
||||
if (hints != nullptr) {
|
||||
host_hints.ai_flags = hints->ai_flags;
|
||||
host_hints.ai_flags = HostAddressInfoFlags(hints->ai_flags);
|
||||
host_hints.ai_family = hints->ai_family;
|
||||
host_hints.ai_socktype = hints->ai_socktype;
|
||||
host_hints.ai_protocol = hints->ai_protocol;
|
||||
@@ -122,7 +165,7 @@ bool CopyHostAddressInfo(const addrinfo* host,
|
||||
BionicFreeAddressInfo(head);
|
||||
return false;
|
||||
}
|
||||
node->ai_flags = current->ai_flags;
|
||||
node->ai_flags = BionicAddressInfoFlags(current->ai_flags);
|
||||
node->ai_family = current->ai_family;
|
||||
node->ai_socktype = current->ai_socktype;
|
||||
node->ai_protocol = current->ai_protocol;
|
||||
@@ -187,6 +230,10 @@ int BionicGetAddressInfo(const char* node, const char* service,
|
||||
if (node != nullptr && IsBlockedCrashReportUploadHost(node)) {
|
||||
return kBionicAddressInfoNameNotFound;
|
||||
}
|
||||
if (hints != nullptr &&
|
||||
(hints->ai_flags & ~SupportedBionicAddressInfoFlags()) != 0) {
|
||||
return kBionicAddressInfoBadFlags;
|
||||
}
|
||||
|
||||
const addrinfo host_hints = HostHints(hints);
|
||||
addrinfo* host_result = nullptr;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "compat/bionic_pthread_create_runtime.h"
|
||||
|
||||
#include "compat/bionic_abi_exports.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
|
||||
@@ -69,24 +73,28 @@ size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
|
||||
return guest_stack_size;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
// Guest pthread_attr_t buffers use the Bionic LP64 layout; they are never a
|
||||
// host pthread_attr_t, so decode the fixed fields directly.
|
||||
int GuestDetachState(const MocktailBionicPthreadAttr& source) noexcept {
|
||||
return (source.flags & kMocktailBionicAttrFlagDetached) != 0
|
||||
? PTHREAD_CREATE_DETACHED
|
||||
: PTHREAD_CREATE_JOINABLE;
|
||||
}
|
||||
|
||||
size_t GuestStackSize(const MocktailBionicPthreadAttr& source) noexcept {
|
||||
return source.stack_size != 0 ? source.stack_size
|
||||
: kBionicLp64DefaultThreadStackSize;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const MocktailBionicPthreadAttr& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(&source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t guard_size = 0;
|
||||
result = pthread_attr_getguardsize(&source, &guard_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setguardsize(destination, guard_size);
|
||||
result = pthread_attr_setguardsize(destination, source.guard_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -95,53 +103,38 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
// pthread_attr_setstack. Copying pthread_attr_getstack's address is unsafe:
|
||||
// glibc represents an automatically allocated stack with a synthetic
|
||||
// address derived from its size.
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstacksize(&source, &stack_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
result = pthread_attr_setstacksize(
|
||||
destination, HostStackSizeForGuest(GuestStackSize(source)));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int inherit_scheduler = PTHREAD_INHERIT_SCHED;
|
||||
result = pthread_attr_getinheritsched(&source, &inherit_scheduler);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
// Bionic applies the attribute's scheduling policy at creation whenever it
|
||||
// differs from the normal policy.
|
||||
if (RejectRealtimeScheduling(source.sched_policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
if (inherit_scheduler == PTHREAD_EXPLICIT_SCHED) {
|
||||
int scheduler_policy = 0;
|
||||
sched_param scheduler_parameters{};
|
||||
result = pthread_attr_getschedpolicy(&source, &scheduler_policy);
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getschedparam(&source, &scheduler_parameters);
|
||||
}
|
||||
if (result == 0 && RejectRealtimeScheduling(scheduler_policy)) {
|
||||
return EPERM;
|
||||
}
|
||||
if (result == 0) {
|
||||
result = pthread_attr_setschedpolicy(destination, scheduler_policy);
|
||||
}
|
||||
if (result == 0) {
|
||||
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
|
||||
}
|
||||
if (source.sched_policy != SCHED_NORMAL) {
|
||||
result = pthread_attr_setschedpolicy(destination, source.sched_policy);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
sched_param scheduler_parameters{};
|
||||
scheduler_parameters.sched_priority = source.sched_priority;
|
||||
result = pthread_attr_setschedparam(destination, &scheduler_parameters);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setinheritsched(destination, PTHREAD_EXPLICIT_SCHED);
|
||||
}
|
||||
return pthread_attr_setinheritsched(destination, inherit_scheduler);
|
||||
|
||||
return pthread_attr_setinheritsched(destination, PTHREAD_INHERIT_SCHED);
|
||||
}
|
||||
|
||||
int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
int CopyRequiredThreadAttributes(const MocktailBionicPthreadAttr& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(&source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -149,32 +142,20 @@ int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
// The retry must retain an explicitly requested guest stack. Falling back
|
||||
// to a null/default host attr is unsafe on musl, whose default stack is much
|
||||
// smaller than Bionic's and can be exhausted by a single libroblox frame.
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstacksize(&source, &stack_size);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
return pthread_attr_setstacksize(
|
||||
destination, HostStackSizeForGuest(GuestStackSize(source)));
|
||||
}
|
||||
|
||||
int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
int ConfigureHostSafeStackFallback(const MocktailBionicPthreadAttr* source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
size_t guest_stack_size = 0;
|
||||
if (source != nullptr) {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
int result = pthread_attr_getdetachstate(source, &detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setdetachstate(destination, detach_state);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_getstacksize(source, &guest_stack_size);
|
||||
int result =
|
||||
pthread_attr_setdetachstate(destination, GuestDetachState(*source));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
guest_stack_size = GuestStackSize(*source);
|
||||
}
|
||||
|
||||
size_t host_default_stack_size = 0;
|
||||
@@ -207,7 +188,8 @@ void ConfigureBionicPthreadThreadInitializer(
|
||||
g_thread_initializer.store(initializer, std::memory_order_release);
|
||||
}
|
||||
|
||||
int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
int CreateBionicPthread(pthread_t* thread,
|
||||
const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument) {
|
||||
if (thread == nullptr || start_routine == nullptr) {
|
||||
return EINVAL;
|
||||
@@ -289,10 +271,124 @@ int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument) {
|
||||
extern "C" {
|
||||
|
||||
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->flags = 0;
|
||||
attr->stack_base = nullptr;
|
||||
attr->stack_size =
|
||||
mocktail::compat::kBionicLp64DefaultThreadStackSize;
|
||||
const long page_size = sysconf(_SC_PAGESIZE);
|
||||
attr->guard_size = page_size > 0 ? static_cast<size_t>(page_size) : 0;
|
||||
attr->sched_policy = SCHED_OTHER;
|
||||
attr->sched_priority = 0;
|
||||
std::memset(attr->reserved, 0, sizeof(attr->reserved));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr) {
|
||||
(void)attr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
|
||||
size_t stack_size) {
|
||||
if (attr == nullptr || stack_size < PTHREAD_STACK_MIN) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->stack_size = stack_size;
|
||||
attr->flags &= ~kMocktailBionicAttrFlagUserStack;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
|
||||
int detach_state) {
|
||||
if (attr == nullptr || (detach_state != PTHREAD_CREATE_DETACHED &&
|
||||
detach_state != PTHREAD_CREATE_JOINABLE)) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (detach_state == PTHREAD_CREATE_DETACHED) {
|
||||
attr->flags |= kMocktailBionicAttrFlagDetached;
|
||||
} else {
|
||||
attr->flags &= ~kMocktailBionicAttrFlagDetached;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
|
||||
const struct sched_param* parameters) {
|
||||
if (attr == nullptr || parameters == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
attr->sched_priority = parameters->sched_priority;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_getattr_np(pthread_t thread,
|
||||
MocktailBionicPthreadAttr* attr) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
pthread_attr_t host_attr;
|
||||
int result = pthread_getattr_np(thread, &host_attr);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
void* stack_base = nullptr;
|
||||
size_t stack_size = 0;
|
||||
result = pthread_attr_getstack(&host_attr, &stack_base, &stack_size);
|
||||
size_t guard_size = 0;
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getguardsize(&host_attr, &guard_size);
|
||||
}
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
if (result == 0) {
|
||||
result = pthread_attr_getdetachstate(&host_attr, &detach_state);
|
||||
}
|
||||
sched_param parameters{};
|
||||
int policy = SCHED_OTHER;
|
||||
if (result == 0) {
|
||||
result = pthread_getschedparam(thread, &policy, ¶meters);
|
||||
}
|
||||
pthread_attr_destroy(&host_attr);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
attr->flags = detach_state == PTHREAD_CREATE_DETACHED
|
||||
? kMocktailBionicAttrFlagDetached
|
||||
: 0;
|
||||
attr->stack_base = stack_base;
|
||||
attr->stack_size = stack_size;
|
||||
attr->guard_size = guard_size;
|
||||
attr->sched_policy = policy;
|
||||
attr->sched_priority = parameters.sched_priority;
|
||||
std::memset(attr->reserved, 0, sizeof(attr->reserved));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
|
||||
void** stack_base, size_t* stack_size) {
|
||||
if (attr == nullptr) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (stack_base != nullptr) {
|
||||
*stack_base = attr->stack_base;
|
||||
}
|
||||
if (stack_size != nullptr) {
|
||||
*stack_size = attr->stack_size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
} // extern "C"
|
||||
|
||||
extern "C" int mocktail_bionic_pthread_create(
|
||||
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
|
||||
void* (*start_routine)(void*), void* argument) {
|
||||
return mocktail::compat::CreateBionicPthread(thread, attr, start_routine,
|
||||
argument);
|
||||
}
|
||||
|
||||
@@ -39,12 +39,23 @@ bool PatchAbsoluteJump(uintptr_t address, void *target) noexcept {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// ldr x16, #8 ; br x16 ; .quad target
|
||||
// x16 is the intra-procedure-call scratch register, free to clobber.
|
||||
unsigned char patch[] = {
|
||||
0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
|
||||
std::memcpy(patch + 8, &target_address, sizeof(target_address));
|
||||
#else
|
||||
unsigned char patch[] = {
|
||||
0x48, 0xb8, // movabs rax, imm64
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0, // jmp rax
|
||||
};
|
||||
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
|
||||
std::memcpy(patch + 2, &target_address, sizeof(target_address));
|
||||
#endif
|
||||
|
||||
const uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
|
||||
const uintptr_t page = address & ~page_mask;
|
||||
|
||||
@@ -6,6 +6,34 @@ namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
constexpr HostAbiProfile kProfiles[] = {
|
||||
// Roblox 2.736.1408 arm64-v8a. Every RVA was derived from the AArch64
|
||||
// payload of the same release as the x86-64 reference above, matched by
|
||||
// allocator internals, init_array call graphs, and static state layout.
|
||||
// Constructor policy mirrors x86-64 by semantic role: the pre-init
|
||||
// entries 0..3 stay skipped, index 4 is mi_process_load (thread init
|
||||
// checkpoint), index 5 is allocator wiring and index 6 is the lazy clock
|
||||
// singleton getter, both excluded under host bridges only.
|
||||
{"ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
|
||||
{{{0x1d53840, HostBridgeKind::kAllocate, "small-allocate"},
|
||||
{0x1d56c34, HostBridgeKind::kUsableSize, "usable-size"},
|
||||
{0x1d6ef80, HostBridgeKind::kReallocate, "reallocate"},
|
||||
{0x1d53568, HostBridgeKind::kAllocate, "allocate"},
|
||||
{0x61d3850, HostBridgeKind::kAlignedAllocate,
|
||||
"aligned-allocate-direct"},
|
||||
{0x1d57328, HostBridgeKind::kFree, "free"}}},
|
||||
6,
|
||||
{0, 0, 0x7296c88, 0x400, 0x1d53c54, 0x1dc76a0,
|
||||
0x71d6c90, 0x71d2c40, 0x400000},
|
||||
{0x1d53568, 0x1d57328},
|
||||
0x6733128,
|
||||
3565,
|
||||
{{{4, 5}, {7, 3565}}},
|
||||
2,
|
||||
{{{4, 3565}}},
|
||||
1,
|
||||
4,
|
||||
{0x2132c5c, 0x71d6d30},
|
||||
HostAllocatorStrategy::kNativeMimalloc},
|
||||
// Roblox 2.736.1408. Derived from the verified 2908 profile and validated
|
||||
// by two isolated no-recovery Vulkan canaries and real game sessions.
|
||||
{"ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::compat {
|
||||
namespace {
|
||||
@@ -611,19 +612,26 @@ std::unique_ptr<OwnedHostAbiProfile> ParseProfile(const Json& root,
|
||||
*error = "external host ABI profile has inconsistent constructor policy";
|
||||
return nullptr;
|
||||
}
|
||||
if (owned->profile.init_array_count <= 5 ||
|
||||
// The verified constructor layout is the same on both guest ABIs: a single
|
||||
// process-load constructor at the thread-initializer checkpoint, two wiring
|
||||
// entries excluded under host bridges, and one contiguous native-mimalloc
|
||||
// replay from the checkpoint to the end of .init_array. Only the checkpoint
|
||||
// index itself differs between payloads.
|
||||
const std::size_t checkpoint =
|
||||
owned->profile.native_mimalloc_thread_initializer_after_constructor;
|
||||
if (owned->profile.init_array_count <= checkpoint + 3 ||
|
||||
owned->profile.constructor_run_range_count != 2 ||
|
||||
owned->profile.constructor_run_ranges[0].begin != 2 ||
|
||||
owned->profile.constructor_run_ranges[0].end_exclusive != 3 ||
|
||||
owned->profile.constructor_run_ranges[1].begin != 5 ||
|
||||
owned->profile.constructor_run_ranges[0].begin != checkpoint ||
|
||||
owned->profile.constructor_run_ranges[0].end_exclusive != checkpoint + 1 ||
|
||||
owned->profile.constructor_run_ranges[1].begin != checkpoint + 3 ||
|
||||
owned->profile.constructor_run_ranges[1].end_exclusive !=
|
||||
owned->profile.init_array_count ||
|
||||
owned->profile.native_mimalloc_constructor_run_range_count != 1 ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin != 2 ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].begin !=
|
||||
checkpoint ||
|
||||
owned->profile.native_mimalloc_constructor_run_ranges[0].end_exclusive !=
|
||||
owned->profile.init_array_count ||
|
||||
owned->profile.native_mimalloc_thread_initializer_after_constructor !=
|
||||
2) {
|
||||
(kGuestElfMachine == EM_X86_64 && checkpoint != 2)) {
|
||||
*error =
|
||||
"external host ABI profile changed the verified constructor policy";
|
||||
return nullptr;
|
||||
@@ -669,9 +677,10 @@ bool ValidateProfileAgainstElf(const std::string& payload_path,
|
||||
}
|
||||
GElf_Ehdr header{};
|
||||
if (gelf_getehdr(elf.get(), &header) == nullptr ||
|
||||
gelf_getclass(elf.get()) != ELFCLASS64 || header.e_machine != EM_X86_64 ||
|
||||
header.e_type != ET_DYN) {
|
||||
*error = "payload must be an x86-64 ET_DYN ELF object";
|
||||
gelf_getclass(elf.get()) != ELFCLASS64 ||
|
||||
header.e_machine != kGuestElfMachine || header.e_type != ET_DYN) {
|
||||
*error = "payload must be a " + std::string(kGuestAbi) +
|
||||
" ET_DYN ELF object";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,8 @@ bool HasSymbols(const SharedLibrary& library,
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options) {
|
||||
BackendCapability CheckAngleLibraries(const AngleProbeOptions& options,
|
||||
bool initialize_display) {
|
||||
if (options.egl_library_path.empty() || options.gles_library_path.empty()) {
|
||||
return Unavailable(
|
||||
"pinned ANGLE EGL and GLES library paths are both required");
|
||||
@@ -104,6 +103,20 @@ BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options) {
|
||||
missing);
|
||||
}
|
||||
|
||||
auto query_string =
|
||||
reinterpret_cast<PFNEGLQUERYSTRINGPROC>(egl.Find("eglQueryString"));
|
||||
const char* extensions = query_string(EGL_NO_DISPLAY, EGL_EXTENSIONS);
|
||||
const std::string extension_list =
|
||||
std::string(" ") + (extensions != nullptr ? extensions : "") + " ";
|
||||
if (extension_list.find(" EGL_ANGLE_platform_angle ") == std::string::npos) {
|
||||
return Unavailable("EGL library does not support EGL_ANGLE_platform_angle");
|
||||
}
|
||||
if (!initialize_display) {
|
||||
return {GraphicsBackendKind::kAngleVulkan, CapabilityState::kLoadable,
|
||||
HardwareAcceleration::kUnknown,
|
||||
"ANGLE EGL/GLES entry points available"};
|
||||
}
|
||||
|
||||
auto get_proc_address =
|
||||
reinterpret_cast<PFNEGLGETPROCADDRESSPROC>(egl.Find("eglGetProcAddress"));
|
||||
auto get_platform_display = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
|
||||
@@ -121,9 +134,6 @@ BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options) {
|
||||
reinterpret_cast<PFNEGLINITIALIZEPROC>(egl.Find("eglInitialize"));
|
||||
auto terminate =
|
||||
reinterpret_cast<PFNEGLTERMINATEPROC>(egl.Find("eglTerminate"));
|
||||
auto query_string =
|
||||
reinterpret_cast<PFNEGLQUERYSTRINGPROC>(egl.Find("eglQueryString"));
|
||||
|
||||
const EGLint device_type =
|
||||
options.allow_software_device
|
||||
? EGL_PLATFORM_ANGLE_DEVICE_TYPE_SWIFTSHADER_ANGLE
|
||||
@@ -173,5 +183,15 @@ BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options) {
|
||||
std::move(detail)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BackendCapability InspectAngleLibraries(const AngleProbeOptions& options) {
|
||||
return CheckAngleLibraries(options, false);
|
||||
}
|
||||
|
||||
BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options) {
|
||||
return CheckAngleLibraries(options, true);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
@@ -2,6 +2,7 @@
|
||||
// Android Vulkan loader ABI -> host Vulkan loader + SDL3 WSI.
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
@@ -1150,16 +1151,28 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Samples the time between successive adapter present calls.
|
||||
// Samples the time between successive adapter present calls, and the CPU
|
||||
// time the presenting thread and the whole process used in it.
|
||||
void RecordProfiledFrame(mocktail::graphics::ChromeTraceWriter* trace,
|
||||
std::uint64_t present_start_ns) {
|
||||
static std::atomic<std::uint64_t> previous_start_ns{0};
|
||||
static mocktail::graphics::FrameCpuSampler cpu_sampler;
|
||||
const std::uint64_t previous =
|
||||
previous_start_ns.exchange(present_start_ns, std::memory_order_relaxed);
|
||||
if (previous != 0 && present_start_ns > previous) {
|
||||
trace->Counter("frame interval (ms)", present_start_ns,
|
||||
static_cast<double>(present_start_ns - previous) / 1e6);
|
||||
}
|
||||
mocktail::graphics::FrameCpuSample cpu;
|
||||
if (cpu_sampler.Sample(static_cast<std::uint64_t>(pthread_self()),
|
||||
mocktail::graphics::TraceThreadCpuNanos(),
|
||||
mocktail::graphics::TraceProcessCpuNanos(), &cpu)) {
|
||||
trace->Counter("process cpu (ms)", present_start_ns, cpu.process_ms);
|
||||
if (cpu.has_thread) {
|
||||
trace->Counter("present thread cpu (ms)", present_start_ns,
|
||||
cpu.thread_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline and shader creation is wrapped only while profiling, so a normal
|
||||
@@ -1241,7 +1254,32 @@ VkResult VKAPI_CALL ProfiledCreatePipelineCache(
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL ProfiledAllocateMemory(
|
||||
VkDevice device, const VkMemoryAllocateInfo* allocate_info,
|
||||
const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) {
|
||||
const auto host = reinterpret_cast<PFN_vkAllocateMemory>(
|
||||
HostDeviceProc(device, "vkAllocateMemory"));
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkAllocateMemory", "memory");
|
||||
const VkResult result = host(device, allocate_info, allocator, memory);
|
||||
scope.Arg("bytes", allocate_info != nullptr
|
||||
? static_cast<std::int64_t>(
|
||||
allocate_info->allocationSize)
|
||||
: 0);
|
||||
scope.Arg("type", allocate_info != nullptr
|
||||
? static_cast<std::int64_t>(
|
||||
allocate_info->memoryTypeIndex)
|
||||
: -1);
|
||||
scope.Arg("result", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Null unless profiling is on and `name` is a profiled device command.
|
||||
// These take precedence over the ETC2 wrappers, so a command both wrap is
|
||||
// timed inside its adapter function instead.
|
||||
PFN_vkVoidFunction ProfileAdapterProc(const char* name) {
|
||||
if (name == nullptr || mocktail::graphics::ActiveProfileTrace() == nullptr) {
|
||||
return nullptr;
|
||||
@@ -1260,6 +1298,9 @@ PFN_vkVoidFunction ProfileAdapterProc(const char* name) {
|
||||
if (std::strcmp(name, "vkCreatePipelineCache") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledCreatePipelineCache);
|
||||
}
|
||||
if (std::strcmp(name, "vkAllocateMemory") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledAllocateMemory);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -2652,11 +2693,15 @@ VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2(
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
|
||||
VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image) {
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateImage", "memory");
|
||||
return State().etc2.CreateImage(device, create_info, allocator, image);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyImage(
|
||||
VkDevice device, VkImage image, const VkAllocationCallbacks* allocator) {
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkDestroyImage", "memory");
|
||||
State().etc2.DestroyImage(device, image, allocator);
|
||||
}
|
||||
|
||||
@@ -2685,6 +2730,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device,
|
||||
VkDeviceSize size,
|
||||
VkMemoryMapFlags flags,
|
||||
void** data) {
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkMapMemory", "memory");
|
||||
return State().etc2.MapMemory(device, memory, offset, size, flags, data);
|
||||
}
|
||||
|
||||
@@ -2696,6 +2743,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2(VkDevice device,
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device,
|
||||
VkDeviceMemory memory) {
|
||||
// Includes waiting for ETC2 decodes that still read this memory.
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkUnmapMemory", "memory");
|
||||
State().etc2.UnmapMemory(device, memory);
|
||||
}
|
||||
|
||||
@@ -2707,6 +2757,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2(VkDevice device,
|
||||
VKAPI_ATTR void VKAPI_CALL vkFreeMemory(
|
||||
VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
// Includes waiting for ETC2 decodes that still read this memory.
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkFreeMemory", "memory");
|
||||
State().etc2.FreeMemory(device, memory, allocator);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
@@ -106,6 +107,53 @@ std::uint64_t TraceClockNanos() {
|
||||
static_cast<std::uint64_t>(now.tv_nsec);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint64_t ClockNanos(clockid_t clock) {
|
||||
timespec now{};
|
||||
clock_gettime(clock, &now);
|
||||
return static_cast<std::uint64_t>(now.tv_sec) * 1'000'000'000ULL +
|
||||
static_cast<std::uint64_t>(now.tv_nsec);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint64_t TraceThreadCpuNanos() {
|
||||
return ClockNanos(CLOCK_THREAD_CPUTIME_ID);
|
||||
}
|
||||
|
||||
std::uint64_t TraceProcessCpuNanos() {
|
||||
return ClockNanos(CLOCK_PROCESS_CPUTIME_ID);
|
||||
}
|
||||
|
||||
bool FrameCpuSampler::Sample(std::uint64_t thread_id,
|
||||
std::uint64_t thread_cpu_ns,
|
||||
std::uint64_t process_cpu_ns,
|
||||
FrameCpuSample* sample) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const bool had_previous = has_previous_;
|
||||
// Clocks are read before the lock, so a reading can arrive after a later
|
||||
// one; a reused thread id restarts its thread clock. Neither may wrap.
|
||||
if (had_previous) {
|
||||
*sample = {};
|
||||
if (process_cpu_ns > previous_process_cpu_ns_) {
|
||||
sample->process_ms =
|
||||
static_cast<double>(process_cpu_ns - previous_process_cpu_ns_) / 1e6;
|
||||
}
|
||||
if (thread_id == previous_thread_id_ &&
|
||||
thread_cpu_ns >= previous_thread_cpu_ns_) {
|
||||
sample->has_thread = true;
|
||||
sample->thread_ms =
|
||||
static_cast<double>(thread_cpu_ns - previous_thread_cpu_ns_) / 1e6;
|
||||
}
|
||||
}
|
||||
has_previous_ = true;
|
||||
previous_thread_id_ = thread_id;
|
||||
previous_thread_cpu_ns_ = thread_cpu_ns;
|
||||
previous_process_cpu_ns_ = std::max(previous_process_cpu_ns_, process_cpu_ns);
|
||||
return had_previous;
|
||||
}
|
||||
|
||||
std::unique_ptr<ChromeTraceWriter> ChromeTraceWriter::Open(
|
||||
const std::filesystem::path& path, std::string* error) {
|
||||
std::FILE* file = std::fopen(path.c_str(), "wb");
|
||||
|
||||
+266
-86
@@ -2,7 +2,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <pthread.h>
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
@@ -42,9 +46,46 @@ int Expand5(int value) { return (value << 3) | (value >> 2); }
|
||||
int Expand6(int value) { return (value << 2) | (value >> 4); }
|
||||
int Expand7(int value) { return (value << 1) | (value >> 6); }
|
||||
|
||||
inline std::uint32_t MakeRgba32(int r, int g, int bl, int a) {
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
__m128i v = _mm_set_epi32(a, bl, g, r);
|
||||
__m128i p16 = _mm_packs_epi32(v, v);
|
||||
__m128i p8 = _mm_packus_epi16(p16, p16);
|
||||
return static_cast<std::uint32_t>(_mm_cvtsi128_si32(p8));
|
||||
#else
|
||||
return static_cast<std::uint32_t>(Clamp255(r)) |
|
||||
(static_cast<std::uint32_t>(Clamp255(g)) << 8) |
|
||||
(static_cast<std::uint32_t>(Clamp255(bl)) << 16) |
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(a)) << 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__SSSE3__) || defined(__x86_64__) || defined(_M_X64)
|
||||
struct ColumnShuffleTable {
|
||||
alignas(16) std::uint8_t masks[256][16];
|
||||
constexpr ColumnShuffleTable() : masks{} {
|
||||
for (int m = 0; m < 16; ++m) {
|
||||
for (int l = 0; l < 16; ++l) {
|
||||
const int idx = (m << 4) | l;
|
||||
for (int p = 0; p < 4; ++p) {
|
||||
const int color_idx = (((m >> p) & 1) << 1) | ((l >> p) & 1);
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
masks[idx][p * 4 + b] = static_cast<std::uint8_t>(color_idx * 4 + b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
inline constexpr ColumnShuffleTable kColShuffleTable{};
|
||||
#endif
|
||||
|
||||
// Decodes one ETC2 RGB block into 16 column-major RGBA texels (x * 4 + y).
|
||||
#if (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(_M_X64))
|
||||
__attribute__((target("ssse3")))
|
||||
#endif
|
||||
void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
std::array<std::uint8_t, 64>* out) {
|
||||
std::uint32_t* out32) {
|
||||
static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1};
|
||||
const bool diff_or_opaque = (b[3] & 2) != 0;
|
||||
const bool opaque = !punchthrough || diff_or_opaque;
|
||||
@@ -53,13 +94,6 @@ void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
auto index_of = [msb, lsb](int texel) {
|
||||
return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1);
|
||||
};
|
||||
auto write = [out](int texel, int r, int g, int bl, int a) {
|
||||
const std::size_t o = static_cast<std::size_t>(texel) * 4;
|
||||
(*out)[o] = Clamp255(r);
|
||||
(*out)[o + 1] = Clamp255(g);
|
||||
(*out)[o + 2] = Clamp255(bl);
|
||||
(*out)[o + 3] = static_cast<std::uint8_t>(a);
|
||||
};
|
||||
|
||||
const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7];
|
||||
const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7];
|
||||
@@ -100,13 +134,13 @@ void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
paint[3][c] = c2[c] - d;
|
||||
}
|
||||
}
|
||||
std::uint32_t paint32[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
paint32[i] = MakeRgba32(paint[i][0], paint[i][1], paint[i][2], 255);
|
||||
}
|
||||
for (int texel = 0; texel < 16; ++texel) {
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
} else {
|
||||
write(texel, paint[index][0], paint[index][1], paint[index][2], 255);
|
||||
}
|
||||
out32[texel] = (!opaque && index == 2) ? 0 : paint32[index];
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -124,9 +158,10 @@ void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
const int bv = Expand6(b[7] & 63);
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2,
|
||||
(x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2,
|
||||
(x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255);
|
||||
out32[x * 4 + y] = MakeRgba32(
|
||||
(x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2,
|
||||
(x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2,
|
||||
(x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -144,39 +179,108 @@ void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
}
|
||||
const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7};
|
||||
const bool flipped = (b[3] & 1) != 0;
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
const int texel = x * 4 + y;
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0);
|
||||
|
||||
// Basis Universal palette precalculation: 8 colors per block instead of 48 additions/clamps.
|
||||
std::uint32_t palette[2][4];
|
||||
for (int sub = 0; sub < 2; ++sub) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
const int modifier =
|
||||
(!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index];
|
||||
write(texel, base[sub][0] + modifier, base[sub][1] + modifier,
|
||||
base[sub][2] + modifier, 255);
|
||||
(!opaque && idx == 0) ? 0 : kEtc1Modifiers[tables[sub]][idx];
|
||||
palette[sub][idx] = MakeRgba32(base[sub][0] + modifier,
|
||||
base[sub][1] + modifier,
|
||||
base[sub][2] + modifier, 255);
|
||||
}
|
||||
}
|
||||
|
||||
if (opaque) {
|
||||
#if defined(__SSSE3__) || defined(__x86_64__) || defined(_M_X64)
|
||||
const __m128i pal0 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(palette[0]));
|
||||
const __m128i pal1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(palette[1]));
|
||||
if (!flipped) {
|
||||
for (int x = 0; x < 2; ++x) {
|
||||
const int combo = (((msb >> (x * 4)) & 15) << 4) | ((lsb >> (x * 4)) & 15);
|
||||
const __m128i mask = _mm_load_si128(
|
||||
reinterpret_cast<const __m128i*>(kColShuffleTable.masks[combo]));
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(out32 + x * 4),
|
||||
_mm_shuffle_epi8(pal0, mask));
|
||||
}
|
||||
for (int x = 2; x < 4; ++x) {
|
||||
const int combo = (((msb >> (x * 4)) & 15) << 4) | ((lsb >> (x * 4)) & 15);
|
||||
const __m128i mask = _mm_load_si128(
|
||||
reinterpret_cast<const __m128i*>(kColShuffleTable.masks[combo]));
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(out32 + x * 4),
|
||||
_mm_shuffle_epi8(pal1, mask));
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
const int combo = (((msb >> (x * 4)) & 15) << 4) | ((lsb >> (x * 4)) & 15);
|
||||
const __m128i mask = _mm_load_si128(
|
||||
reinterpret_cast<const __m128i*>(kColShuffleTable.masks[combo]));
|
||||
const __m128i col0 = _mm_shuffle_epi8(pal0, mask);
|
||||
const __m128i col1 = _mm_shuffle_epi8(pal1, mask);
|
||||
const __m128i blended =
|
||||
_mm_unpacklo_epi64(col0, _mm_srli_si128(col1, 8));
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(out32 + x * 4), blended);
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (!flipped) {
|
||||
for (int x = 0; x < 2; ++x) {
|
||||
const std::uint32_t* pal = palette[0];
|
||||
out32[x * 4 + 0] = pal[index_of(x * 4 + 0)];
|
||||
out32[x * 4 + 1] = pal[index_of(x * 4 + 1)];
|
||||
out32[x * 4 + 2] = pal[index_of(x * 4 + 2)];
|
||||
out32[x * 4 + 3] = pal[index_of(x * 4 + 3)];
|
||||
}
|
||||
for (int x = 2; x < 4; ++x) {
|
||||
const std::uint32_t* pal = palette[1];
|
||||
out32[x * 4 + 0] = pal[index_of(x * 4 + 0)];
|
||||
out32[x * 4 + 1] = pal[index_of(x * 4 + 1)];
|
||||
out32[x * 4 + 2] = pal[index_of(x * 4 + 2)];
|
||||
out32[x * 4 + 3] = pal[index_of(x * 4 + 3)];
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
out32[x * 4 + 0] = palette[0][index_of(x * 4 + 0)];
|
||||
out32[x * 4 + 1] = palette[0][index_of(x * 4 + 1)];
|
||||
out32[x * 4 + 2] = palette[1][index_of(x * 4 + 2)];
|
||||
out32[x * 4 + 3] = palette[1][index_of(x * 4 + 3)];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
const int texel = x * 4 + y;
|
||||
const int index = index_of(texel);
|
||||
if (index == 2) {
|
||||
out32[texel] = 0;
|
||||
} else {
|
||||
const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0);
|
||||
out32[texel] = palette[sub][index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EAC 3-bit index for column-major texel (x * 4 + y).
|
||||
int EacIndex(const std::uint8_t* b, int texel) {
|
||||
inline std::uint64_t ReadEacBits(const std::uint8_t* b) {
|
||||
std::uint64_t bits = 0;
|
||||
for (int i = 2; i < 8; ++i) {
|
||||
bits = (bits << 8) | b[i];
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
inline int EacIndexFromBits(std::uint64_t bits, int texel) {
|
||||
return static_cast<int>((bits >> (45 - texel * 3)) & 7);
|
||||
}
|
||||
|
||||
int EacModifier(const std::uint8_t* b, int texel) {
|
||||
return kEacModifiers[b[1] & 15][EacIndex(b, texel)];
|
||||
}
|
||||
|
||||
std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) {
|
||||
const int multiplier = b[1] >> 4;
|
||||
const int modifier = EacModifier(b, texel);
|
||||
inline std::uint16_t DecodeR11Modifier(const std::uint8_t* b, int multiplier,
|
||||
const int* modifier_table,
|
||||
int modifier_idx, bool is_signed) {
|
||||
const int modifier = modifier_table[modifier_idx];
|
||||
const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier;
|
||||
if (!is_signed) {
|
||||
const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047);
|
||||
@@ -308,12 +412,18 @@ class DecodePool {
|
||||
run_ = run;
|
||||
context_ = context;
|
||||
unclaimed_ = helpers;
|
||||
running_ = helpers;
|
||||
running_.store(helpers, std::memory_order_relaxed);
|
||||
}
|
||||
for (std::size_t i = 0; i < helpers; ++i) {
|
||||
ready_.notify_one();
|
||||
}
|
||||
ready_.notify_all();
|
||||
run(context);
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
done_.wait(lock, [this] { return running_ == 0; });
|
||||
if (running_.load(std::memory_order_acquire) > 0) {
|
||||
std::unique_lock<std::mutex> lock(done_mutex_);
|
||||
done_.wait(lock, [this] {
|
||||
return running_.load(std::memory_order_acquire) == 0;
|
||||
});
|
||||
}
|
||||
run_ = nullptr;
|
||||
context_ = nullptr;
|
||||
return true;
|
||||
@@ -343,29 +453,33 @@ class DecodePool {
|
||||
}
|
||||
|
||||
void WorkerLoop() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
for (;;) {
|
||||
ready_.wait(lock, [this] { return unclaimed_ > 0; });
|
||||
--unclaimed_;
|
||||
void (*run)(void*) = run_;
|
||||
void* context = context_;
|
||||
lock.unlock();
|
||||
void (*run)(void*) = nullptr;
|
||||
void* context = nullptr;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
ready_.wait(lock, [this] { return unclaimed_ > 0; });
|
||||
--unclaimed_;
|
||||
run = run_;
|
||||
context = context_;
|
||||
}
|
||||
run(context);
|
||||
lock.lock();
|
||||
if (--running_ == 0) {
|
||||
done_.notify_all();
|
||||
if (running_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
std::lock_guard<std::mutex> lock(done_mutex_);
|
||||
done_.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex batch_mutex_;
|
||||
std::mutex mutex_;
|
||||
std::mutex done_mutex_;
|
||||
std::condition_variable ready_;
|
||||
std::condition_variable done_;
|
||||
void (*run_)(void*) = nullptr;
|
||||
void* context_ = nullptr;
|
||||
std::size_t unclaimed_ = 0;
|
||||
std::size_t running_ = 0;
|
||||
std::atomic<std::size_t> running_{0};
|
||||
std::size_t threads_ = 0;
|
||||
};
|
||||
|
||||
@@ -378,6 +492,16 @@ void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
}
|
||||
DecodeBandQueue queue;
|
||||
std::vector<DecodeBand>& bands = queue.bands;
|
||||
std::size_t estimated_bands = 0;
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
const EtcDecodeJob& job = jobs[index];
|
||||
const std::uint32_t blocks_wide = (job.width + 3) / 4;
|
||||
const std::uint32_t blocks_high = (job.height + 3) / 4;
|
||||
const std::uint32_t rows_per_band = static_cast<std::uint32_t>(
|
||||
std::max<std::uint64_t>(1, kBlocksPerBand / (blocks_wide ? blocks_wide : 1)));
|
||||
estimated_bands += (blocks_high + rows_per_band - 1) / (rows_per_band ? rows_per_band : 1);
|
||||
}
|
||||
bands.reserve(estimated_bands);
|
||||
std::uint64_t total_blocks = 0;
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
EtcDecodeJob& job = jobs[index];
|
||||
@@ -439,48 +563,104 @@ bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
const std::size_t texel_bytes = EtcDecodedTexelBytes(format);
|
||||
const std::size_t block_bytes = EtcBlockBytes(format);
|
||||
|
||||
std::array<std::uint8_t, 64> rgba{};
|
||||
alignas(16) std::uint32_t cols[16];
|
||||
const std::uint64_t end_block_row =
|
||||
static_cast<std::uint64_t>(first_block_row) + block_row_count;
|
||||
const bool is_rgba_family = (format == EtcFormat::kEtc2Rgb8 ||
|
||||
format == EtcFormat::kEtc2Rgb8A1 ||
|
||||
format == EtcFormat::kEtc2Rgba8);
|
||||
|
||||
for (std::uint64_t by = first_block_row; by < end_block_row; ++by) {
|
||||
for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) {
|
||||
const std::uint8_t* block =
|
||||
source + (by * blocks_wide + bx) * block_bytes;
|
||||
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
|
||||
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba);
|
||||
} else if (format == EtcFormat::kEtc2Rgba8) {
|
||||
DecodeRgbBlock(block + 8, false, &rgba);
|
||||
}
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint8_t* out =
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgb8:
|
||||
case EtcFormat::kEtc2Rgb8A1:
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
std::copy_n(rgba.data() + texel * 4, 4, out);
|
||||
if (format == EtcFormat::kEtc2Rgba8) {
|
||||
const int alpha =
|
||||
block[0] + EacModifier(block, texel) * (block[1] >> 4);
|
||||
out[3] = Clamp255(alpha);
|
||||
}
|
||||
break;
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed: {
|
||||
const bool is_signed = format == EtcFormat::kEacR11Signed ||
|
||||
format == EtcFormat::kEacRg11Signed;
|
||||
const int channels = texel_bytes / 2;
|
||||
for (int channel = 0; channel < channels; ++channel) {
|
||||
const std::uint16_t value =
|
||||
DecodeR11(block + channel * 8, texel, is_signed);
|
||||
out[channel * 2] = static_cast<std::uint8_t>(value);
|
||||
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
break;
|
||||
const bool interior = (by * 4 + 4 <= height) && (bx * 4 + 4 <= width);
|
||||
|
||||
if (is_rgba_family) {
|
||||
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
|
||||
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, cols);
|
||||
} else {
|
||||
DecodeRgbBlock(block + 8, false, cols);
|
||||
const std::uint64_t alpha_bits = ReadEacBits(block);
|
||||
const int alpha_base = block[0];
|
||||
const int alpha_multiplier = block[1] >> 4;
|
||||
const auto& alpha_table = kEacModifiers[block[1] & 15];
|
||||
std::uint8_t alpha_palette[8];
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
alpha_palette[i] =
|
||||
Clamp255(alpha_base + alpha_table[i] * alpha_multiplier);
|
||||
}
|
||||
for (int texel = 0; texel < 16; ++texel) {
|
||||
cols[texel] =
|
||||
(cols[texel] & 0x00FFFFFF) |
|
||||
(static_cast<std::uint32_t>(
|
||||
alpha_palette[EacIndexFromBits(alpha_bits, texel)])
|
||||
<< 24);
|
||||
}
|
||||
}
|
||||
|
||||
if (interior) {
|
||||
std::uint32_t* dst_row = reinterpret_cast<std::uint32_t*>(
|
||||
destination + (by * 4 * width + bx * 4) * 4);
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
__m128 r0 = _mm_load_ps(reinterpret_cast<const float*>(cols));
|
||||
__m128 r1 = _mm_load_ps(reinterpret_cast<const float*>(cols + 4));
|
||||
__m128 r2 = _mm_load_ps(reinterpret_cast<const float*>(cols + 8));
|
||||
__m128 r3 = _mm_load_ps(reinterpret_cast<const float*>(cols + 12));
|
||||
_MM_TRANSPOSE4_PS(r0, r1, r2, r3);
|
||||
_mm_storeu_ps(reinterpret_cast<float*>(dst_row), r0);
|
||||
dst_row += width;
|
||||
_mm_storeu_ps(reinterpret_cast<float*>(dst_row), r1);
|
||||
dst_row += width;
|
||||
_mm_storeu_ps(reinterpret_cast<float*>(dst_row), r2);
|
||||
dst_row += width;
|
||||
_mm_storeu_ps(reinterpret_cast<float*>(dst_row), r3);
|
||||
#else
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
dst_row[0] = cols[y];
|
||||
dst_row[1] = cols[4 + y];
|
||||
dst_row[2] = cols[8 + y];
|
||||
dst_row[3] = cols[12 + y];
|
||||
dst_row += width;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint32_t* out = reinterpret_cast<std::uint32_t*>(
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * 4);
|
||||
*out = cols[texel];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const bool is_signed = format == EtcFormat::kEacR11Signed ||
|
||||
format == EtcFormat::kEacRg11Signed;
|
||||
const int channels = static_cast<int>(texel_bytes / 2);
|
||||
std::uint64_t channel_bits[2] = {0, 0};
|
||||
std::uint16_t channel_palette[2][8];
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
const std::uint8_t* cb = block + c * 8;
|
||||
channel_bits[c] = ReadEacBits(cb);
|
||||
const int mult = cb[1] >> 4;
|
||||
const int* table = kEacModifiers[cb[1] & 15];
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
channel_palette[c][i] =
|
||||
DecodeR11Modifier(cb, mult, table, i, is_signed);
|
||||
}
|
||||
}
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint8_t* out =
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
|
||||
for (int channel = 0; channel < channels; ++channel) {
|
||||
const std::uint16_t value =
|
||||
channel_palette[channel][EacIndexFromBits(channel_bits[channel],
|
||||
texel)];
|
||||
out[channel * 2] = static_cast<std::uint8_t>(value);
|
||||
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,20 +72,33 @@ void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
static_cast<std::size_t>(source.width) * source.height * 4) {
|
||||
return;
|
||||
}
|
||||
ResampleRgba(source.pixels.data(), source.width, source.height, width,
|
||||
height, destination);
|
||||
ResampleRgba(source.pixels.data(), source.width, source.height,
|
||||
static_cast<std::size_t>(source.width) * 4, width, height,
|
||||
destination);
|
||||
}
|
||||
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination) {
|
||||
ResampleRgba(source, source_width, source_height,
|
||||
static_cast<std::size_t>(source_width) * 4, width, height,
|
||||
destination);
|
||||
}
|
||||
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::size_t source_stride_bytes,
|
||||
std::uint32_t width, std::uint32_t height,
|
||||
std::uint8_t* destination) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0 || source_width == 0 || source_height == 0) {
|
||||
height == 0 || source_width == 0 || source_height == 0 ||
|
||||
source_stride_bytes < static_cast<std::size_t>(source_width) * 4) {
|
||||
return;
|
||||
}
|
||||
// Source box of destination column x is [columns[2x], columns[2x + 1]).
|
||||
thread_local std::vector<std::uint32_t> columns;
|
||||
columns.resize(static_cast<std::size_t>(width) * 2);
|
||||
if (columns.size() < static_cast<std::size_t>(width) * 2) {
|
||||
columns.resize(static_cast<std::size_t>(width) * 2);
|
||||
}
|
||||
bool one_texel_columns = true;
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const std::uint32_t x0 = static_cast<std::uint32_t>(
|
||||
@@ -115,11 +128,11 @@ void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
previous_y0 = y0;
|
||||
previous_y1 = y1;
|
||||
if (one_texel_columns && y1 == y0 + 1) {
|
||||
const std::uint8_t* source_row = source + y0 * source_width * 4;
|
||||
const std::uint8_t* source_row = source + y0 * source_stride_bytes;
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
std::memcpy(out_row + static_cast<std::size_t>(x) * 4,
|
||||
source_row + columns[2 * static_cast<std::size_t>(x)] * 4,
|
||||
4);
|
||||
std::uint32_t pixel;
|
||||
std::memcpy(&pixel, source_row + columns[2 * static_cast<std::size_t>(x)] * 4, 4);
|
||||
std::memcpy(out_row + static_cast<std::size_t>(x) * 4, &pixel, 4);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -128,8 +141,9 @@ void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
const std::uint64_t x1 = columns[2 * static_cast<std::size_t>(x) + 1];
|
||||
std::uint64_t sum[4] = {0, 0, 0, 0};
|
||||
for (std::uint64_t sy = y0; sy < y1; ++sy) {
|
||||
const std::uint8_t* row_ptr = source + sy * source_stride_bytes;
|
||||
for (std::uint64_t sx = x0; sx < x1; ++sx) {
|
||||
const std::uint8_t* pixel = source + (sy * source_width + sx) * 4;
|
||||
const std::uint8_t* pixel = row_ptr + sx * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
sum[channel] += pixel[channel];
|
||||
}
|
||||
@@ -137,9 +151,16 @@ void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
}
|
||||
const std::uint64_t count = (y1 - y0) * (x1 - x0);
|
||||
std::uint8_t* out = destination + (static_cast<std::size_t>(y) * width + x) * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out[channel] =
|
||||
static_cast<std::uint8_t>((sum[channel] + count / 2) / count);
|
||||
if (count == 1) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out[channel] = static_cast<std::uint8_t>(sum[channel]);
|
||||
}
|
||||
} else {
|
||||
const std::uint64_t half = count / 2;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out[channel] =
|
||||
static_cast<std::uint8_t>((sum[channel] + half) / count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out) {
|
||||
@@ -211,19 +215,34 @@ bool IsColorFormat(EtcFormat format) {
|
||||
}
|
||||
|
||||
// A host-visible transfer buffer that stays mapped for its whole life.
|
||||
struct Staging {
|
||||
// Uploads take ranges of it front to back, and once every range has been
|
||||
// released it starts over from the front. Creating a buffer and destroying
|
||||
// it each cost the driver a kernel call of a tenth of a millisecond or more,
|
||||
// so a join's thousands of uploads share a few blocks instead.
|
||||
struct StagingBlock {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkDeviceSize capacity = 0;
|
||||
std::uint8_t* mapped = nullptr;
|
||||
// First byte not yet handed out, and the ranges command buffers hold.
|
||||
VkDeviceSize head = 0;
|
||||
std::size_t live = 0;
|
||||
};
|
||||
|
||||
// Idle staging buffers kept for reuse. Creating, allocating, binding and
|
||||
// mapping one costs hundreds of microseconds in the driver, so a burst of
|
||||
// texture uploads would otherwise stall the frame that records it.
|
||||
constexpr VkDeviceSize kMaxIdleStagingBytes = 64 * 1024 * 1024;
|
||||
constexpr std::size_t kMaxIdleStagingBuffers = 64;
|
||||
// One recorded copy's range of a staging block.
|
||||
struct Staging {
|
||||
StagingBlock* block = nullptr;
|
||||
VkDeviceSize offset = 0;
|
||||
};
|
||||
|
||||
// Ranges start on this boundary. It meets the bufferOffset rule of every
|
||||
// host format and keeps ranges written by different threads off shared cache
|
||||
// lines.
|
||||
constexpr VkDeviceSize kStagingAlignment = 256;
|
||||
// Empty blocks kept for reuse, counted in block sizes; the rest are
|
||||
// destroyed.
|
||||
constexpr VkDeviceSize kIdleStagingBlocks = 4;
|
||||
// Decode batches kept for their buffers once they complete.
|
||||
constexpr std::size_t kMaxSpareBatches = 8;
|
||||
|
||||
@@ -247,9 +266,6 @@ class RawBuffer {
|
||||
std::unique_ptr<std::uint8_t[]> data_;
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
// A request may take an idle buffer up to this size, or up to 4x its own
|
||||
// size when larger, so small uploads share buffers without pinning big ones.
|
||||
constexpr VkDeviceSize kStagingReuseSlack = 64 * 1024;
|
||||
|
||||
struct PendingUpload {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
@@ -327,9 +343,8 @@ void LogFailure(const char* message) {
|
||||
}
|
||||
}
|
||||
|
||||
bool CreateStaging(const HostDevice& dev, VkDeviceSize size, Staging* staging,
|
||||
std::uint8_t** mapped) {
|
||||
*mapped = nullptr;
|
||||
bool CreateStagingBlock(const HostDevice& dev, VkDeviceSize size,
|
||||
StagingBlock* block) {
|
||||
if (dev.create_buffer == nullptr || dev.allocate_memory == nullptr ||
|
||||
dev.get_buffer_memory_requirements == nullptr ||
|
||||
dev.bind_buffer_memory == nullptr || dev.map_memory == nullptr ||
|
||||
@@ -379,12 +394,53 @@ bool CreateStaging(const HostDevice& dev, VkDeviceSize size, Staging* staging,
|
||||
dev.free_memory(dev.device, memory, nullptr);
|
||||
return false;
|
||||
}
|
||||
*mapped = static_cast<std::uint8_t*>(data);
|
||||
*staging = {dev.device, buffer, memory, size, *mapped};
|
||||
block->device = dev.device;
|
||||
block->buffer = buffer;
|
||||
block->memory = memory;
|
||||
block->capacity = size;
|
||||
block->mapped = static_cast<std::uint8_t*>(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Freeing mapped memory unmaps it implicitly.
|
||||
#if defined(__x86_64__)
|
||||
__attribute__((target("sse4.1")))
|
||||
inline void CopyHostVisibleMemory(std::uint8_t* dst, const std::uint8_t* src,
|
||||
std::size_t bytes) {
|
||||
if ((reinterpret_cast<std::uintptr_t>(src) & 15) != 0 || bytes < 16) {
|
||||
std::memcpy(dst, src, bytes);
|
||||
return;
|
||||
}
|
||||
std::size_t i = 0;
|
||||
for (; i + 64 <= bytes; i += 64) {
|
||||
__m128i c0 = _mm_stream_load_si128(
|
||||
reinterpret_cast<__m128i*>(const_cast<std::uint8_t*>(src + i)));
|
||||
__m128i c1 = _mm_stream_load_si128(
|
||||
reinterpret_cast<__m128i*>(const_cast<std::uint8_t*>(src + i + 16)));
|
||||
__m128i c2 = _mm_stream_load_si128(
|
||||
reinterpret_cast<__m128i*>(const_cast<std::uint8_t*>(src + i + 32)));
|
||||
__m128i c3 = _mm_stream_load_si128(
|
||||
reinterpret_cast<__m128i*>(const_cast<std::uint8_t*>(src + i + 48)));
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i), c0);
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i + 16), c1);
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i + 32), c2);
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i + 48), c3);
|
||||
}
|
||||
for (; i + 16 <= bytes; i += 16) {
|
||||
__m128i c = _mm_stream_load_si128(
|
||||
reinterpret_cast<__m128i*>(const_cast<std::uint8_t*>(src + i)));
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i), c);
|
||||
}
|
||||
if (i < bytes) {
|
||||
std::memcpy(dst + i, src + i, bytes - i);
|
||||
}
|
||||
}
|
||||
#else
|
||||
inline void CopyHostVisibleMemory(std::uint8_t* dst, const std::uint8_t* src,
|
||||
std::size_t bytes) {
|
||||
std::memcpy(dst, src, bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Packs each upload's compressed bytes into `destination`, dropping the row
|
||||
// and layer padding the application may have asked for. Sources are parallel
|
||||
// to `uploads`.
|
||||
@@ -398,17 +454,18 @@ void GatherCompressed(const std::vector<PendingUpload>& uploads,
|
||||
std::uint8_t* target = destination + offset;
|
||||
const VkDeviceSize compressed_layer = upload.compressed / upload.layers;
|
||||
if (upload.source_span == upload.compressed) {
|
||||
std::memcpy(target, source, upload.compressed);
|
||||
CopyHostVisibleMemory(target, source, upload.compressed);
|
||||
} else {
|
||||
const VkDeviceSize block_rows =
|
||||
(static_cast<VkDeviceSize>(upload.height) + 3) / 4;
|
||||
const VkDeviceSize packed_row = compressed_layer / block_rows;
|
||||
for (std::uint32_t layer = 0; layer < upload.layers; ++layer) {
|
||||
for (VkDeviceSize row = 0; row < block_rows; ++row) {
|
||||
std::memcpy(target + layer * compressed_layer + row * packed_row,
|
||||
source + layer * upload.source_layer_stride +
|
||||
row * upload.source_row_stride,
|
||||
packed_row);
|
||||
CopyHostVisibleMemory(
|
||||
target + layer * compressed_layer + row * packed_row,
|
||||
source + layer * upload.source_layer_stride +
|
||||
row * upload.source_row_stride,
|
||||
packed_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,12 +491,13 @@ void RecordHostWriteBarrier(const HostDevice& dev,
|
||||
nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
void DestroyStaging(const HostDevice& dev, const Staging& staging) {
|
||||
// Freeing mapped memory unmaps it implicitly.
|
||||
void DestroyStagingBlock(const HostDevice& dev, const StagingBlock& block) {
|
||||
if (dev.destroy_buffer != nullptr) {
|
||||
dev.destroy_buffer(dev.device, staging.buffer, nullptr);
|
||||
dev.destroy_buffer(dev.device, block.buffer, nullptr);
|
||||
}
|
||||
if (dev.free_memory != nullptr) {
|
||||
dev.free_memory(dev.device, staging.memory, nullptr);
|
||||
dev.free_memory(dev.device, block.memory, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +661,9 @@ bool PlanRegions(const ImageRecord& record, VkDevice device, VkBuffer source,
|
||||
} // namespace
|
||||
|
||||
struct VulkanEtc2Emulation::State {
|
||||
explicit State(VkDeviceSize block_bytes)
|
||||
: staging_block_bytes(block_bytes) {}
|
||||
|
||||
std::mutex mutex;
|
||||
std::mutex devices_mutex;
|
||||
std::unordered_map<VkPhysicalDevice, bool> physical_devices;
|
||||
@@ -612,8 +673,10 @@ struct VulkanEtc2Emulation::State {
|
||||
std::unordered_map<VkDeviceMemory, Mapping> mappings;
|
||||
std::unordered_map<VkCommandBuffer, CommandRecord> commands;
|
||||
std::atomic<bool> has_commands{false};
|
||||
std::vector<Staging> idle_staging;
|
||||
VkDeviceSize idle_staging_bytes = 0;
|
||||
// Blocks of every device, in creation order. Guarded by `mutex`; a block's
|
||||
// address is stable until it is destroyed.
|
||||
const VkDeviceSize staging_block_bytes;
|
||||
std::vector<std::unique_ptr<StagingBlock>> staging_blocks;
|
||||
// Heap buffers reused across submits; they only grow.
|
||||
std::mutex scratch_mutex;
|
||||
RawBuffer scratch_source;
|
||||
@@ -691,21 +754,11 @@ struct VulkanEtc2Emulation::State {
|
||||
upload.target);
|
||||
return;
|
||||
}
|
||||
const std::uint8_t* source = decoded;
|
||||
std::vector<std::uint8_t> cropped;
|
||||
if (upload.source_width != upload.width ||
|
||||
upload.source_height != upload.height) {
|
||||
const std::size_t row = static_cast<std::size_t>(upload.source_width) * 4;
|
||||
cropped.resize(row * upload.source_height);
|
||||
for (std::uint32_t y = 0; y < upload.source_height; ++y) {
|
||||
std::memcpy(cropped.data() + y * row,
|
||||
decoded + static_cast<std::size_t>(y) * upload.width * 4,
|
||||
row);
|
||||
}
|
||||
source = cropped.data();
|
||||
}
|
||||
ResampleRgba(source, upload.source_width, upload.source_height,
|
||||
upload.target_width, upload.target_height, upload.target);
|
||||
const std::size_t source_stride =
|
||||
static_cast<std::size_t>(upload.width) * 4;
|
||||
ResampleRgba(decoded, upload.source_width, upload.source_height,
|
||||
source_stride, upload.target_width, upload.target_height,
|
||||
upload.target);
|
||||
} else {
|
||||
std::memcpy(upload.target, decoded, upload.decoded);
|
||||
}
|
||||
@@ -765,51 +818,78 @@ struct VulkanEtc2Emulation::State {
|
||||
static_cast<unsigned>(threads));
|
||||
}
|
||||
|
||||
// Takes the smallest idle buffer that fits, or creates one.
|
||||
// Takes `size` bytes from the first block of the device with room for
|
||||
// them, or from a new block of at least `staging_block_bytes`.
|
||||
bool AcquireStaging(const HostDevice& dev, VkDeviceSize size,
|
||||
Staging* staging) {
|
||||
const VkDeviceSize aligned =
|
||||
(size + kStagingAlignment - 1) / kStagingAlignment * kStagingAlignment;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
const VkDeviceSize limit = std::max(size * 4, kStagingReuseSlack);
|
||||
auto best = idle_staging.end();
|
||||
for (auto it = idle_staging.begin(); it != idle_staging.end(); ++it) {
|
||||
if (it->device == dev.device && it->capacity >= size &&
|
||||
it->capacity <= limit &&
|
||||
(best == idle_staging.end() || it->capacity < best->capacity)) {
|
||||
best = it;
|
||||
for (const auto& block : staging_blocks) {
|
||||
if (block->device == dev.device &&
|
||||
block->capacity - block->head >= aligned) {
|
||||
*staging = {block.get(), block->head};
|
||||
block->head += aligned;
|
||||
++block->live;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (best != idle_staging.end()) {
|
||||
*staging = *best;
|
||||
idle_staging_bytes -= best->capacity;
|
||||
*best = idle_staging.back();
|
||||
idle_staging.pop_back();
|
||||
return true;
|
||||
}
|
||||
// Created outside the lock, which the decode workers also take.
|
||||
auto block = std::make_unique<StagingBlock>();
|
||||
{
|
||||
const VkDeviceSize capacity = std::max(staging_block_bytes, aligned);
|
||||
TraceScope trace_scope(ActiveProfileTrace(), "etc2 staging create",
|
||||
"texture");
|
||||
trace_scope.Arg("bytes", static_cast<std::int64_t>(capacity));
|
||||
if (!CreateStagingBlock(dev, capacity, block.get())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::uint8_t* mapped = nullptr;
|
||||
return CreateStaging(dev, size, staging, &mapped);
|
||||
block->head = aligned;
|
||||
block->live = 1;
|
||||
*staging = {block.get(), 0};
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
staging_blocks.push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keeps released buffers for reuse up to the idle limits and destroys
|
||||
// the rest.
|
||||
void ReleaseStaging(std::vector<Staging> released) {
|
||||
std::vector<Staging> doomed;
|
||||
// Returns ranges to their blocks. A block with no ranges left starts over
|
||||
// from its front; empty blocks beyond kIdleStagingBlocks block sizes are
|
||||
// destroyed.
|
||||
void ReleaseStaging(const std::vector<Staging>& released) {
|
||||
std::vector<std::unique_ptr<StagingBlock>> doomed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
for (const Staging& staging : released) {
|
||||
if (idle_staging.size() < kMaxIdleStagingBuffers &&
|
||||
idle_staging_bytes + staging.capacity <= kMaxIdleStagingBytes) {
|
||||
idle_staging.push_back(staging);
|
||||
idle_staging_bytes += staging.capacity;
|
||||
} else {
|
||||
doomed.push_back(staging);
|
||||
if (--staging.block->live == 0) {
|
||||
staging.block->head = 0;
|
||||
}
|
||||
}
|
||||
const VkDeviceSize idle_limit = kIdleStagingBlocks * staging_block_bytes;
|
||||
VkDeviceSize idle_bytes = 0;
|
||||
for (auto it = staging_blocks.begin(); it != staging_blocks.end();) {
|
||||
if ((*it)->live == 0 && idle_bytes + (*it)->capacity > idle_limit) {
|
||||
doomed.push_back(std::move(*it));
|
||||
it = staging_blocks.erase(it);
|
||||
continue;
|
||||
}
|
||||
if ((*it)->live == 0) {
|
||||
idle_bytes += (*it)->capacity;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
for (const Staging& staging : doomed) {
|
||||
if (const HostDevice* dev = Find(staging.device); dev != nullptr) {
|
||||
DestroyStaging(*dev, staging);
|
||||
if (doomed.empty()) {
|
||||
return;
|
||||
}
|
||||
TraceScope trace_scope(ActiveProfileTrace(), "etc2 staging destroy",
|
||||
"texture");
|
||||
trace_scope.Arg("buffers", static_cast<std::int64_t>(doomed.size()));
|
||||
for (const auto& block : doomed) {
|
||||
if (const HostDevice* dev = Find(block->device); dev != nullptr) {
|
||||
DestroyStagingBlock(*dev, *block);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1123,8 @@ struct VulkanEtc2Emulation::State {
|
||||
}
|
||||
};
|
||||
|
||||
VulkanEtc2Emulation::VulkanEtc2Emulation() : state_(new State) {}
|
||||
VulkanEtc2Emulation::VulkanEtc2Emulation(VkDeviceSize staging_block_bytes)
|
||||
: state_(new State(staging_block_bytes)) {}
|
||||
|
||||
VulkanEtc2Emulation::~VulkanEtc2Emulation() {
|
||||
state_->StopDispatcher();
|
||||
@@ -1163,27 +1244,22 @@ void VulkanEtc2Emulation::RegisterDevice(
|
||||
void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
// Nothing may free staging or the semaphore while a decode still writes it.
|
||||
state_->DrainDecodes();
|
||||
std::vector<Staging> doomed;
|
||||
std::vector<std::unique_ptr<StagingBlock>> doomed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
for (auto it = state_->commands.begin(); it != state_->commands.end();) {
|
||||
const bool owned = std::any_of(
|
||||
it->second.staging.begin(), it->second.staging.end(),
|
||||
[device](const Staging& staging) { return staging.device == device; });
|
||||
if (owned) {
|
||||
doomed.insert(doomed.end(), it->second.staging.begin(),
|
||||
it->second.staging.end());
|
||||
it = state_->commands.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
[device](const Staging& staging) {
|
||||
return staging.block->device == device;
|
||||
});
|
||||
it = owned ? state_->commands.erase(it) : std::next(it);
|
||||
}
|
||||
for (auto it = state_->idle_staging.begin();
|
||||
it != state_->idle_staging.end();) {
|
||||
if (it->device == device) {
|
||||
state_->idle_staging_bytes -= it->capacity;
|
||||
doomed.push_back(*it);
|
||||
it = state_->idle_staging.erase(it);
|
||||
auto& blocks = state_->staging_blocks;
|
||||
for (auto it = blocks.begin(); it != blocks.end();) {
|
||||
if ((*it)->device == device) {
|
||||
doomed.push_back(std::move(*it));
|
||||
it = blocks.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
@@ -1195,8 +1271,8 @@ void VulkanEtc2Emulation::DestroyDevice(VkDevice device) {
|
||||
std::memory_order_release);
|
||||
}
|
||||
if (const HostDevice* dev = state_->Find(device); dev != nullptr) {
|
||||
for (const Staging& staging : doomed) {
|
||||
DestroyStaging(*dev, staging);
|
||||
for (const auto& block : doomed) {
|
||||
DestroyStagingBlock(*dev, *block);
|
||||
}
|
||||
if (dev->timeline != VK_NULL_HANDLE && dev->destroy_semaphore != nullptr) {
|
||||
dev->destroy_semaphore(device, dev->timeline, nullptr);
|
||||
@@ -1442,11 +1518,14 @@ void VulkanEtc2Emulation::CmdCopyBufferToImage(
|
||||
return;
|
||||
}
|
||||
for (PendingUpload& upload : uploads) {
|
||||
upload.target = staging.mapped + upload.target_offset;
|
||||
upload.target = staging.block->mapped + staging.offset + upload.target_offset;
|
||||
}
|
||||
for (VkBufferImageCopy& region : rewritten) {
|
||||
region.bufferOffset += staging.offset;
|
||||
}
|
||||
state_->Record(command_buffer, std::move(uploads), staging);
|
||||
RecordHostWriteBarrier(*dev, command_buffer);
|
||||
dev->copy_buffer_to_image(command_buffer, staging.buffer, destination,
|
||||
dev->copy_buffer_to_image(command_buffer, staging.block->buffer, destination,
|
||||
layout, region_count, rewritten.data());
|
||||
}
|
||||
|
||||
@@ -1478,12 +1557,15 @@ void VulkanEtc2Emulation::CmdCopyBufferToImage2(
|
||||
return;
|
||||
}
|
||||
for (PendingUpload& upload : uploads) {
|
||||
upload.target = staging.mapped + upload.target_offset;
|
||||
upload.target = staging.block->mapped + staging.offset + upload.target_offset;
|
||||
}
|
||||
for (VkBufferImageCopy2& region : rewritten) {
|
||||
region.bufferOffset += staging.offset;
|
||||
}
|
||||
state_->Record(command_buffer, std::move(uploads), staging);
|
||||
RecordHostWriteBarrier(*dev, command_buffer);
|
||||
VkCopyBufferToImageInfo2 host_info = *info;
|
||||
host_info.srcBuffer = staging.buffer;
|
||||
host_info.srcBuffer = staging.block->buffer;
|
||||
host_info.pRegions = rewritten.data();
|
||||
dev->copy_buffer_to_image2(command_buffer, &host_info);
|
||||
}
|
||||
@@ -1652,9 +1734,7 @@ Etc2SubmitWait VulkanEtc2Emulation::PrepareSubmit(
|
||||
collect(command_buffers[index]);
|
||||
const auto record = state_->commands.find(command_buffers[index]);
|
||||
if (record != state_->commands.end()) {
|
||||
const std::vector<VkCommandBuffer> secondaries =
|
||||
record->second.secondaries;
|
||||
for (VkCommandBuffer secondary : secondaries) {
|
||||
for (VkCommandBuffer secondary : record->second.secondaries) {
|
||||
collect(secondary);
|
||||
}
|
||||
}
|
||||
@@ -1834,7 +1914,18 @@ void VulkanEtc2Emulation::ReleaseCommandBuffer(VkCommandBuffer command_buffer) {
|
||||
ticket = record->second.last_ticket;
|
||||
}
|
||||
// Waited on without holding the state lock, which the decode itself takes.
|
||||
// Only waits long enough to matter are traced; this runs for every
|
||||
// command buffer the application resets.
|
||||
ChromeTraceWriter* const trace = ActiveProfileTrace();
|
||||
const std::uint64_t wait_start_ns = trace != nullptr ? TraceClockNanos() : 0;
|
||||
state_->WaitForTicket(ticket);
|
||||
if (trace != nullptr) {
|
||||
const std::uint64_t wait_end_ns = TraceClockNanos();
|
||||
if (wait_end_ns - wait_start_ns >= 100'000) {
|
||||
trace->Slice("etc2 release wait", "texture", wait_start_ns, wait_end_ns,
|
||||
{{"ticket", static_cast<std::int64_t>(ticket)}});
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Staging> doomed;
|
||||
{
|
||||
|
||||
@@ -749,8 +749,10 @@ void VulkanTextOverlayCompositor::DestroyDevice(VkDevice device) {
|
||||
if (iterator == impl_->devices.end()) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> overlay_lock((*iterator)->overlay_mutex);
|
||||
impl_->DestroyDeviceResources(iterator->get());
|
||||
{
|
||||
std::lock_guard<std::mutex> overlay_lock((*iterator)->overlay_mutex);
|
||||
impl_->DestroyDeviceResources(iterator->get());
|
||||
}
|
||||
impl_->devices.erase(iterator);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "jni_fields.h"
|
||||
|
||||
#include "jni_references.h"
|
||||
|
||||
namespace jnivm::internal {
|
||||
namespace {
|
||||
std::unordered_set<std::string> g_field_names;
|
||||
std::unordered_map<std::string, jobject> g_static_object_fields;
|
||||
} // namespace
|
||||
|
||||
jfieldID StoreFieldId(const char* name) {
|
||||
if (name == nullptr) return nullptr;
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
const auto stored = g_field_names.emplace(name).first;
|
||||
// unordered_set preserves references to its elements across rehashes.
|
||||
return reinterpret_cast<jfieldID>(const_cast<char*>(stored->c_str()));
|
||||
}
|
||||
|
||||
void SetStaticObjectFieldRaw(const char* name, jobject value) {
|
||||
if (name == nullptr) return;
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
RetainJniReference(value);
|
||||
auto& field = g_static_object_fields[name];
|
||||
const jobject previous = field;
|
||||
field = value;
|
||||
ReleaseJniReference(previous);
|
||||
}
|
||||
|
||||
jobject StaticObjectFieldValue(const char* name) {
|
||||
if (name == nullptr) return nullptr;
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
const auto found = g_static_object_fields.find(name);
|
||||
return found == g_static_object_fields.end()
|
||||
? nullptr
|
||||
: NewLocalJniReference(found->second);
|
||||
}
|
||||
|
||||
void SetObjectFieldRaw(jobject obj, const char* field_name, jobject value) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (pseudo_object && field_name) {
|
||||
RetainJniReference(value);
|
||||
auto it = pseudo_object->object_fields.find(field_name);
|
||||
jobject prev =
|
||||
(it != pseudo_object->object_fields.end()) ? it->second : nullptr;
|
||||
pseudo_object->object_fields[field_name] = value;
|
||||
if (prev != nullptr) {
|
||||
ReleaseJniReference(prev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetIntFieldRaw(jobject obj, const char* field_name, jint value) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (pseudo_object && field_name) {
|
||||
pseudo_object->int_fields[field_name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SetLongFieldRaw(jobject obj, const char* field_name, jlong value) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (pseudo_object && field_name) {
|
||||
pseudo_object->long_fields[field_name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SetFloatFieldRaw(jobject obj, const char* field_name, jfloat value) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (pseudo_object && field_name) {
|
||||
pseudo_object->float_fields[field_name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SetBooleanFieldRaw(jobject obj, const char* field_name, jboolean value) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (pseudo_object && field_name) {
|
||||
pseudo_object->boolean_fields[field_name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
jobject ObjectFieldValue(jobject obj, const char* field_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !field_name) {
|
||||
return nullptr;
|
||||
}
|
||||
auto it = pseudo_object->object_fields.find(field_name);
|
||||
return it == pseudo_object->object_fields.end()
|
||||
? nullptr
|
||||
: NewLocalJniReference(it->second);
|
||||
}
|
||||
|
||||
jint IntFieldValue(jobject obj, const char* field_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !field_name) {
|
||||
return 0;
|
||||
}
|
||||
auto it = pseudo_object->int_fields.find(field_name);
|
||||
return it == pseudo_object->int_fields.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
jboolean BooleanFieldValue(jobject obj, const char* field_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !field_name) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
auto it = pseudo_object->boolean_fields.find(field_name);
|
||||
return it == pseudo_object->boolean_fields.end() ? JNI_FALSE : it->second;
|
||||
}
|
||||
|
||||
jlong LongFieldValue(jobject obj, const char* field_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !field_name) {
|
||||
return 0;
|
||||
}
|
||||
auto it = pseudo_object->long_fields.find(field_name);
|
||||
return it == pseudo_object->long_fields.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
jfloat FloatFieldValue(jobject obj, const char* field_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !field_name) {
|
||||
return 0.0f;
|
||||
}
|
||||
auto it = pseudo_object->float_fields.find(field_name);
|
||||
return it == pseudo_object->float_fields.end() ? 0.0f : it->second;
|
||||
}
|
||||
|
||||
} // namespace jnivm::internal
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef MOCKTAIL_JNIVM_JNI_FIELDS_H_
|
||||
#define MOCKTAIL_JNIVM_JNI_FIELDS_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
namespace jnivm::internal {
|
||||
|
||||
// Field names are interned for the lifetime of the process. Object getters
|
||||
// return independently owned local references; setters retain their values.
|
||||
jfieldID StoreFieldId(const char* name);
|
||||
void SetStaticObjectFieldRaw(const char* name, jobject value);
|
||||
jobject StaticObjectFieldValue(const char* name);
|
||||
void SetObjectFieldRaw(jobject obj, const char* name, jobject value);
|
||||
void SetIntFieldRaw(jobject obj, const char* name, jint value);
|
||||
void SetLongFieldRaw(jobject obj, const char* name, jlong value);
|
||||
void SetFloatFieldRaw(jobject obj, const char* name, jfloat value);
|
||||
void SetBooleanFieldRaw(jobject obj, const char* name, jboolean value);
|
||||
jobject ObjectFieldValue(jobject obj, const char* name);
|
||||
jint IntFieldValue(jobject obj, const char* name);
|
||||
jlong LongFieldValue(jobject obj, const char* name);
|
||||
jfloat FloatFieldValue(jobject obj, const char* name);
|
||||
jboolean BooleanFieldValue(jobject obj, const char* name);
|
||||
|
||||
} // namespace jnivm::internal
|
||||
#endif
|
||||
@@ -0,0 +1,448 @@
|
||||
#include "jni_references.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace jnivm {
|
||||
// Encoded handles are also read by the guest ABI boundary.
|
||||
void* my_segment[100000] = {};
|
||||
int g_jni_ref_index = 1;
|
||||
} // namespace jnivm
|
||||
|
||||
namespace jnivm::internal {
|
||||
|
||||
enum class SegmentType : uint8_t {
|
||||
kEmpty = 0,
|
||||
kObject = 1,
|
||||
kClass = 2,
|
||||
};
|
||||
|
||||
// Class handles are interned by binary name. Real JNI resolves the same class
|
||||
// to the same runtime class object, and class handles are never released, so
|
||||
// every FindClass/GetObjectClass used to consume a fresh permanent segment
|
||||
// slot and exhaust the table during long sessions.
|
||||
// Each frame counts how many local references it holds per handle, so
|
||||
// DeleteLocalRef costs one hash lookup per open frame.
|
||||
using LocalFrame = std::unordered_map<jobject, uint32_t>;
|
||||
thread_local std::vector<LocalFrame> g_local_frames;
|
||||
std::recursive_mutex g_jni_state_mutex;
|
||||
std::unordered_map<jobject, std::unique_ptr<Object>> g_object_storage;
|
||||
std::unordered_map<jobject, uint32_t> g_jni_ref_counts;
|
||||
std::shared_ptr<void> g_segment_owners[kJniSegmentCapacity];
|
||||
std::unordered_set<jclass> g_known_classes;
|
||||
// One handle per Class. Class slots are never freed, so the table holds at
|
||||
// most one slot per distinct class.
|
||||
std::unordered_map<const Class*, jclass> g_class_handles;
|
||||
std::unordered_map<std::string, jobject> g_singleton_objects;
|
||||
std::unordered_set<jobject> g_singleton_handles;
|
||||
std::unordered_map<std::string, std::shared_ptr<Class>> g_fallback_classes;
|
||||
std::unordered_set<jstring> g_known_strings;
|
||||
std::unordered_map<jarray, std::unique_ptr<PseudoArray>> g_array_storage;
|
||||
static std::atomic<uint8_t> g_segment_types[kJniSegmentCapacity] = {};
|
||||
std::vector<int> g_free_slots;
|
||||
|
||||
static bool g_shutting_down_jnivm = false;
|
||||
|
||||
struct JniVmShutdownHook {
|
||||
JniVmShutdownHook() {
|
||||
std::atexit([]() { g_shutting_down_jnivm = true; });
|
||||
}
|
||||
};
|
||||
static JniVmShutdownHook g_shutdown_hook;
|
||||
|
||||
bool TraceEnabled() {
|
||||
static const bool enabled = std::getenv("MOCKTAIL_JNI_TRACE") != nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
jobject JniHandleFromIndex(uint32_t index) {
|
||||
return reinterpret_cast<jobject>(static_cast<uintptr_t>(index)
|
||||
<< kJniHandleShift);
|
||||
}
|
||||
|
||||
uint32_t JniIndexFromHandle(jobject obj) {
|
||||
return static_cast<uint32_t>(reinterpret_cast<uintptr_t>(obj) >>
|
||||
kJniHandleShift);
|
||||
}
|
||||
|
||||
void EnsureLocalFrame() {
|
||||
if (g_local_frames.empty()) {
|
||||
g_local_frames.emplace_back();
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterLocalRef(jobject obj) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
EnsureLocalFrame();
|
||||
++g_local_frames.back()[obj];
|
||||
}
|
||||
|
||||
void UnregisterLocalRef(jobject obj) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (auto frame = g_local_frames.rbegin(); frame != g_local_frames.rend();
|
||||
++frame) {
|
||||
auto it = frame->find(obj);
|
||||
if (it != frame->end()) {
|
||||
if (--it->second == 0) {
|
||||
frame->erase(it);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A detached thread can no longer use its local references, so they go the
|
||||
// way ART drops them on detach.
|
||||
void ReleaseThreadLocalRefs() {
|
||||
std::vector<LocalFrame> frames = std::move(g_local_frames);
|
||||
g_local_frames.clear();
|
||||
for (const LocalFrame& frame : frames) {
|
||||
for (const auto& [obj, count] : frame) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
ReleaseJniReference(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RetainJniReference(jobject obj) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto it = g_jni_ref_counts.find(obj);
|
||||
if (it != g_jni_ref_counts.end()) {
|
||||
++it->second;
|
||||
} else {
|
||||
g_jni_ref_counts[obj] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ReportHandleTableFull();
|
||||
|
||||
int AllocateSegmentSlot(void* value, std::shared_ptr<void> owner = nullptr,
|
||||
SegmentType type = SegmentType::kObject) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
int index = 0;
|
||||
if (!g_free_slots.empty()) {
|
||||
index = g_free_slots.back();
|
||||
g_free_slots.pop_back();
|
||||
} else if (g_jni_ref_index > 0 &&
|
||||
static_cast<uint32_t>(g_jni_ref_index) < kJniSegmentCapacity) {
|
||||
index = g_jni_ref_index++;
|
||||
} else {
|
||||
ReportHandleTableFull();
|
||||
return 0;
|
||||
}
|
||||
my_segment[index] = value;
|
||||
g_segment_owners[index] = std::move(owner);
|
||||
g_segment_types[index].store(static_cast<uint8_t>(type),
|
||||
std::memory_order_release);
|
||||
return index;
|
||||
}
|
||||
|
||||
std::shared_ptr<Class> FallbackClassForName(const std::string& class_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto it = g_fallback_classes.find(class_name);
|
||||
if (it != g_fallback_classes.end()) {
|
||||
return it->second;
|
||||
}
|
||||
auto cls = std::make_shared<Class>(class_name);
|
||||
g_fallback_classes[class_name] = cls;
|
||||
return cls;
|
||||
}
|
||||
|
||||
std::shared_ptr<Class> ClassFromJClass(jclass clazz) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (!clazz) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t index = JniIndexFromHandle(reinterpret_cast<jobject>(clazz));
|
||||
Class* cls = nullptr;
|
||||
if (index > 0 && index < kJniSegmentCapacity) {
|
||||
cls = reinterpret_cast<Class*>(my_segment[index]);
|
||||
if (g_segment_owners[index]) {
|
||||
return std::static_pointer_cast<Class>(g_segment_owners[index]);
|
||||
}
|
||||
} else {
|
||||
cls = reinterpret_cast<Class*>(clazz);
|
||||
}
|
||||
if (!cls) {
|
||||
return nullptr;
|
||||
}
|
||||
return FallbackClassForName(cls->GetName());
|
||||
}
|
||||
|
||||
jclass StoreClass(std::shared_ptr<Class> cls) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (!cls) {
|
||||
return nullptr;
|
||||
}
|
||||
Class* raw_ptr = cls.get();
|
||||
if (const auto cached = g_class_handles.find(raw_ptr);
|
||||
cached != g_class_handles.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
int index = AllocateSegmentSlot(raw_ptr, cls, SegmentType::kClass);
|
||||
if (index <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
jclass handle = reinterpret_cast<jclass>(
|
||||
JniHandleFromIndex(static_cast<uint32_t>(index)));
|
||||
g_known_classes.insert(handle);
|
||||
g_class_handles.emplace(raw_ptr, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
jobject StoreObject(std::unique_ptr<Object> object) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
Object* raw_ptr = object.get();
|
||||
int index = AllocateSegmentSlot(raw_ptr, nullptr, SegmentType::kObject);
|
||||
if (index <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
jobject handle = JniHandleFromIndex(static_cast<uint32_t>(index));
|
||||
g_object_storage[handle] = std::move(object);
|
||||
g_jni_ref_counts[handle] = 1;
|
||||
RegisterLocalRef(handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
void ReleaseJniReference(jobject obj) {
|
||||
if (obj == nullptr || g_shutting_down_jnivm) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (g_shutting_down_jnivm) {
|
||||
return;
|
||||
}
|
||||
auto ref_it = g_jni_ref_counts.find(obj);
|
||||
if (ref_it != g_jni_ref_counts.end()) {
|
||||
if (ref_it->second > 1) {
|
||||
--ref_it->second;
|
||||
return;
|
||||
}
|
||||
g_jni_ref_counts.erase(ref_it);
|
||||
}
|
||||
|
||||
auto arr_it = g_array_storage.find(reinterpret_cast<jarray>(obj));
|
||||
if (arr_it != g_array_storage.end()) {
|
||||
std::unique_ptr<PseudoArray> dying_array = std::move(arr_it->second);
|
||||
std::vector<jobject> elements = std::move(dying_array->objects);
|
||||
g_array_storage.erase(arr_it);
|
||||
// Release after detaching the array: elements may recursively erase other
|
||||
// arrays. Keep dying_array alive until those releases have completed.
|
||||
for (jobject element : elements) {
|
||||
ReleaseJniReference(element);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (g_known_classes.find(reinterpret_cast<jclass>(obj)) !=
|
||||
g_known_classes.end()) {
|
||||
return;
|
||||
}
|
||||
if (g_singleton_handles.count(obj) != 0) {
|
||||
return;
|
||||
}
|
||||
const uint32_t index = JniIndexFromHandle(obj);
|
||||
if (index > 0 && index < kJniSegmentCapacity) {
|
||||
g_segment_types[index].store(static_cast<uint8_t>(SegmentType::kEmpty),
|
||||
std::memory_order_release);
|
||||
my_segment[index] = nullptr;
|
||||
g_segment_owners[index].reset();
|
||||
g_free_slots.push_back(static_cast<int>(index));
|
||||
}
|
||||
g_known_strings.erase(reinterpret_cast<jstring>(obj));
|
||||
std::unique_ptr<Object> dying_object;
|
||||
auto obj_it = g_object_storage.find(obj);
|
||||
if (obj_it != g_object_storage.end()) {
|
||||
dying_object = std::move(obj_it->second);
|
||||
g_object_storage.erase(obj_it);
|
||||
}
|
||||
}
|
||||
|
||||
PseudoJavaObject* PseudoObjectFromRef(jobject obj) {
|
||||
if (__builtin_expect(obj == nullptr, 0)) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t index = JniIndexFromHandle(obj);
|
||||
if (__builtin_expect(index > 0 && index < kJniSegmentCapacity, 1)) {
|
||||
const uint8_t type = g_segment_types[index].load(std::memory_order_acquire);
|
||||
if (__builtin_expect(type == static_cast<uint8_t>(SegmentType::kObject),
|
||||
1)) {
|
||||
void* raw_ptr = my_segment[index];
|
||||
if (__builtin_expect(raw_ptr != nullptr, 1)) {
|
||||
return static_cast<PseudoJavaObject*>(
|
||||
reinterpret_cast<Object*>(raw_ptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jobject MakeObjectForClass(const std::string& class_name) {
|
||||
auto cls = FallbackClassForName(class_name);
|
||||
return StoreObject(std::make_unique<PseudoJavaObject>(std::move(cls)));
|
||||
}
|
||||
|
||||
jobject MakeObject(jclass clazz) {
|
||||
auto cls = ClassFromJClass(clazz);
|
||||
if (!cls) {
|
||||
cls = FallbackClassForName("java/lang/Object");
|
||||
}
|
||||
return StoreObject(std::make_unique<PseudoJavaObject>(std::move(cls)));
|
||||
}
|
||||
|
||||
jobject SingletonObject(const std::string& class_name) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto it = g_singleton_objects.find(class_name);
|
||||
if (it != g_singleton_objects.end()) {
|
||||
return it->second;
|
||||
}
|
||||
jobject object = MakeObjectForClass(class_name);
|
||||
g_singleton_objects[class_name] = object;
|
||||
g_singleton_handles.insert(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
std::string_view ObjectClassName(jobject obj) {
|
||||
PseudoJavaObject* pseudo_object = PseudoObjectFromRef(obj);
|
||||
if (!pseudo_object || !pseudo_object->GetClass()) {
|
||||
return {};
|
||||
}
|
||||
return pseudo_object->GetClass()->GetName();
|
||||
}
|
||||
|
||||
void MarkSingletonHandle(jobject obj) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
g_singleton_handles.insert(obj);
|
||||
}
|
||||
|
||||
// Logs once per process which classes hold the live handles, so the leak
|
||||
// behind a later null FindClass or NewStringUTF can be named.
|
||||
void ReportHandleTableFull() {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
static bool reported = false;
|
||||
if (reported) {
|
||||
return;
|
||||
}
|
||||
reported = true;
|
||||
std::unordered_map<std::string_view, std::size_t> live_by_class;
|
||||
for (const auto& entry : g_object_storage) {
|
||||
std::string_view name = ObjectClassName(entry.first);
|
||||
++live_by_class[name.empty() ? std::string_view("<unknown>") : name];
|
||||
}
|
||||
std::vector<std::pair<std::string_view, std::size_t>> ranked(
|
||||
live_by_class.begin(), live_by_class.end());
|
||||
const std::size_t shown = std::min<std::size_t>(ranked.size(), 10);
|
||||
std::partial_sort(ranked.begin(), ranked.begin() + shown, ranked.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.second > b.second;
|
||||
});
|
||||
fprintf(stderr,
|
||||
"[JNI] handle table full (%u slots): %zu objects, %zu classes, "
|
||||
"%zu arrays live; new objects return null\n",
|
||||
kJniSegmentCapacity, g_object_storage.size(),
|
||||
g_class_handles.size(), g_array_storage.size());
|
||||
for (std::size_t i = 0; i < shown; ++i) {
|
||||
fprintf(stderr, "[JNI] %zu x %.*s\n", ranked[i].second,
|
||||
static_cast<int>(ranked[i].first.size()), ranked[i].first.data());
|
||||
}
|
||||
}
|
||||
|
||||
PseudoArray* ArrayFromRef(jarray array) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto it = g_array_storage.find(array);
|
||||
return it == g_array_storage.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
jbyteArray MakeByteArray(jsize len) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto array = std::make_unique<PseudoArray>();
|
||||
if (len > 0) {
|
||||
array->bytes.resize(static_cast<std::size_t>(len));
|
||||
}
|
||||
jbyteArray ref = reinterpret_cast<jbyteArray>(array.get());
|
||||
g_array_storage[ref] = std::move(array);
|
||||
g_jni_ref_counts[reinterpret_cast<jobject>(ref)] = 1;
|
||||
RegisterLocalRef(reinterpret_cast<jobject>(ref));
|
||||
return ref;
|
||||
}
|
||||
|
||||
jfloatArray MakeFloatArray(jsize len) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto array = std::make_unique<PseudoArray>();
|
||||
if (len > 0) {
|
||||
array->floats.resize(static_cast<std::size_t>(len));
|
||||
}
|
||||
jfloatArray ref = reinterpret_cast<jfloatArray>(array.get());
|
||||
g_array_storage[ref] = std::move(array);
|
||||
g_jni_ref_counts[reinterpret_cast<jobject>(ref)] = 1;
|
||||
RegisterLocalRef(reinterpret_cast<jobject>(ref));
|
||||
return ref;
|
||||
}
|
||||
|
||||
jobjectArray MakeObjectArray(jsize len, jobject init) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto array = std::make_unique<PseudoArray>();
|
||||
if (len > 0) {
|
||||
array->objects.resize(static_cast<std::size_t>(len), init);
|
||||
if (init != nullptr) {
|
||||
for (jsize index = 0; index < len; ++index) {
|
||||
RetainJniReference(init);
|
||||
}
|
||||
}
|
||||
}
|
||||
jobjectArray ref = reinterpret_cast<jobjectArray>(array.get());
|
||||
g_array_storage[ref] = std::move(array);
|
||||
g_jni_ref_counts[reinterpret_cast<jobject>(ref)] = 1;
|
||||
RegisterLocalRef(reinterpret_cast<jobject>(ref));
|
||||
return ref;
|
||||
}
|
||||
|
||||
PseudoJavaObject::~PseudoJavaObject() {
|
||||
for (const auto& field : object_fields) ReleaseJniReference(field.second);
|
||||
}
|
||||
|
||||
jobject NewLocalJniReference(jobject obj) {
|
||||
if (obj != nullptr) {
|
||||
RetainJniReference(obj);
|
||||
RegisterLocalRef(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
void DeleteLocalJniReference(jobject obj) {
|
||||
UnregisterLocalRef(obj);
|
||||
ReleaseJniReference(obj);
|
||||
}
|
||||
|
||||
void PushLocalJniFrame() {
|
||||
EnsureLocalFrame();
|
||||
g_local_frames.emplace_back();
|
||||
}
|
||||
|
||||
jobject PopLocalJniFrame(jobject result) {
|
||||
EnsureLocalFrame();
|
||||
RetainJniReference(result);
|
||||
LocalFrame frame = std::move(g_local_frames.back());
|
||||
if (g_local_frames.size() > 1)
|
||||
g_local_frames.pop_back();
|
||||
else
|
||||
g_local_frames.back().clear();
|
||||
for (const auto& [obj, count] : frame) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
ReleaseJniReference(obj);
|
||||
}
|
||||
}
|
||||
RegisterLocalRef(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace jnivm::internal
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef MOCKTAIL_JNIVM_JNI_REFERENCES_H_
|
||||
#define MOCKTAIL_JNIVM_JNI_REFERENCES_H_
|
||||
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
namespace jnivm::internal {
|
||||
|
||||
// Shared by reference ownership and object/field access. Callers retaining a
|
||||
// raw PseudoJavaObject pointer must keep its JNI reference alive.
|
||||
extern std::recursive_mutex g_jni_state_mutex;
|
||||
extern std::unordered_set<jstring> g_known_strings;
|
||||
|
||||
inline constexpr uint32_t kJniSegmentCapacity = 100000;
|
||||
inline constexpr uint32_t kJniHandleShift = 16;
|
||||
|
||||
struct PseudoArray {
|
||||
std::vector<jbyte> bytes;
|
||||
std::vector<jfloat> floats;
|
||||
std::vector<jobject> objects;
|
||||
};
|
||||
|
||||
struct PseudoJavaObject : Object {
|
||||
explicit PseudoJavaObject(std::shared_ptr<Class> cls)
|
||||
: Object(std::move(cls)) {}
|
||||
|
||||
~PseudoJavaObject() override;
|
||||
|
||||
std::unordered_map<std::string, jobject> object_fields;
|
||||
std::unordered_map<std::string, jint> int_fields;
|
||||
std::unordered_map<std::string, jlong> long_fields;
|
||||
std::unordered_map<std::string, jfloat> float_fields;
|
||||
std::unordered_map<std::string, jboolean> boolean_fields;
|
||||
};
|
||||
|
||||
bool TraceEnabled();
|
||||
void RegisterLocalRef(jobject obj);
|
||||
void UnregisterLocalRef(jobject obj);
|
||||
void ReleaseThreadLocalRefs();
|
||||
void RetainJniReference(jobject obj);
|
||||
void ReleaseJniReference(jobject obj);
|
||||
jobject NewLocalJniReference(jobject obj);
|
||||
void DeleteLocalJniReference(jobject obj);
|
||||
void PushLocalJniFrame();
|
||||
jobject PopLocalJniFrame(jobject result);
|
||||
std::shared_ptr<Class> FallbackClassForName(const std::string& name);
|
||||
std::shared_ptr<Class> ClassFromJClass(jclass clazz);
|
||||
jclass StoreClass(std::shared_ptr<Class> cls);
|
||||
jobject StoreObject(std::unique_ptr<Object> object);
|
||||
PseudoJavaObject* PseudoObjectFromRef(jobject obj);
|
||||
jobject MakeObjectForClass(const std::string& name);
|
||||
jobject MakeObject(jclass clazz);
|
||||
jobject SingletonObject(const std::string& name);
|
||||
// Marks a VM-owned handle so detaching its creating thread does not free it.
|
||||
void MarkSingletonHandle(jobject obj);
|
||||
std::string_view ObjectClassName(jobject obj);
|
||||
PseudoArray* ArrayFromRef(jarray array);
|
||||
jbyteArray MakeByteArray(jsize len);
|
||||
jfloatArray MakeFloatArray(jsize len);
|
||||
jobjectArray MakeObjectArray(jsize len, jobject init);
|
||||
|
||||
} // namespace jnivm::internal
|
||||
#endif
|
||||
@@ -0,0 +1,381 @@
|
||||
#include "jni_strings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
#include "jni_references.h"
|
||||
|
||||
namespace jnivm::internal {
|
||||
namespace {
|
||||
constexpr jchar kEmptyUtf16[] = {0};
|
||||
|
||||
void AppendUtf8CodePoint(std::uint32_t code_point, std::string* output) {
|
||||
if (code_point <= 0x7f) {
|
||||
output->push_back(static_cast<char>(code_point));
|
||||
} else if (code_point <= 0x7ff) {
|
||||
output->push_back(static_cast<char>(0xc0 | (code_point >> 6)));
|
||||
output->push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
|
||||
} else if (code_point <= 0xffff) {
|
||||
output->push_back(static_cast<char>(0xe0 | (code_point >> 12)));
|
||||
output->push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
|
||||
output->push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
|
||||
} else {
|
||||
output->push_back(static_cast<char>(0xf0 | (code_point >> 18)));
|
||||
output->push_back(static_cast<char>(0x80 | ((code_point >> 12) & 0x3f)));
|
||||
output->push_back(static_cast<char>(0x80 | ((code_point >> 6) & 0x3f)));
|
||||
output->push_back(static_cast<char>(0x80 | (code_point & 0x3f)));
|
||||
}
|
||||
}
|
||||
|
||||
std::string Utf16ToUtf8(const std::vector<jchar>& utf16) {
|
||||
std::string output;
|
||||
output.reserve(utf16.size());
|
||||
for (std::size_t index = 0; index < utf16.size(); ++index) {
|
||||
std::uint32_t code_point = utf16[index];
|
||||
if (code_point >= 0xd800 && code_point <= 0xdbff &&
|
||||
index + 1 < utf16.size() && utf16[index + 1] >= 0xdc00 &&
|
||||
utf16[index + 1] <= 0xdfff) {
|
||||
code_point =
|
||||
0x10000 + ((code_point - 0xd800) << 10) + (utf16[++index] - 0xdc00);
|
||||
} else if (code_point >= 0xd800 && code_point <= 0xdfff) {
|
||||
code_point = 0xfffd;
|
||||
}
|
||||
AppendUtf8CodePoint(code_point, &output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::string Utf16ToModifiedUtf8(const std::vector<jchar>& utf16,
|
||||
std::size_t begin = 0,
|
||||
std::size_t count = std::string::npos) {
|
||||
std::string output;
|
||||
if (begin >= utf16.size()) {
|
||||
return output;
|
||||
}
|
||||
const std::size_t end =
|
||||
std::min(utf16.size(), begin + std::min(count, utf16.size() - begin));
|
||||
output.reserve((end - begin) * 3);
|
||||
for (std::size_t index = begin; index < end; ++index) {
|
||||
const std::uint32_t code_unit = utf16[index];
|
||||
if (code_unit == 0) {
|
||||
output.push_back(static_cast<char>(0xc0));
|
||||
output.push_back(static_cast<char>(0x80));
|
||||
} else if (code_unit <= 0x7f) {
|
||||
output.push_back(static_cast<char>(code_unit));
|
||||
} else if (code_unit <= 0x7ff) {
|
||||
output.push_back(static_cast<char>(0xc0 | (code_unit >> 6)));
|
||||
output.push_back(static_cast<char>(0x80 | (code_unit & 0x3f)));
|
||||
} else {
|
||||
output.push_back(static_cast<char>(0xe0 | (code_unit >> 12)));
|
||||
output.push_back(static_cast<char>(0x80 | ((code_unit >> 6) & 0x3f)));
|
||||
output.push_back(static_cast<char>(0x80 | (code_unit & 0x3f)));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<jchar> ModifiedUtf8ToUtf16(const char* input) {
|
||||
std::vector<jchar> output;
|
||||
if (input == nullptr) {
|
||||
return output;
|
||||
}
|
||||
const auto* bytes = reinterpret_cast<const std::uint8_t*>(input);
|
||||
const std::size_t size = std::strlen(input);
|
||||
for (std::size_t index = 0; index < size;) {
|
||||
const std::uint8_t first = bytes[index];
|
||||
std::uint32_t code_point = 0xfffd;
|
||||
std::size_t consumed = 1;
|
||||
if (first != 0 && first <= 0x7f) {
|
||||
code_point = first;
|
||||
} else if ((first & 0xe0) == 0xc0 && index + 1 < size &&
|
||||
(bytes[index + 1] & 0xc0) == 0x80) {
|
||||
code_point = ((first & 0x1f) << 6) | (bytes[index + 1] & 0x3f);
|
||||
if (code_point == 0 || code_point >= 0x80) {
|
||||
consumed = 2;
|
||||
} else {
|
||||
code_point = 0xfffd;
|
||||
}
|
||||
} else if ((first & 0xf0) == 0xe0 && index + 2 < size &&
|
||||
(bytes[index + 1] & 0xc0) == 0x80 &&
|
||||
(bytes[index + 2] & 0xc0) == 0x80) {
|
||||
code_point = ((first & 0x0f) << 12) | ((bytes[index + 1] & 0x3f) << 6) |
|
||||
(bytes[index + 2] & 0x3f);
|
||||
if (code_point >= 0x800) {
|
||||
consumed = 3;
|
||||
} else {
|
||||
code_point = 0xfffd;
|
||||
}
|
||||
} else if ((first & 0xf8) == 0xf0 && index + 3 < size &&
|
||||
(bytes[index + 1] & 0xc0) == 0x80 &&
|
||||
(bytes[index + 2] & 0xc0) == 0x80 &&
|
||||
(bytes[index + 3] & 0xc0) == 0x80) {
|
||||
code_point = ((first & 0x07) << 18) | ((bytes[index + 1] & 0x3f) << 12) |
|
||||
((bytes[index + 2] & 0x3f) << 6) | (bytes[index + 3] & 0x3f);
|
||||
if (code_point >= 0x10000 && code_point <= 0x10ffff) {
|
||||
consumed = 4;
|
||||
} else {
|
||||
code_point = 0xfffd;
|
||||
}
|
||||
}
|
||||
if (code_point <= 0xffff) {
|
||||
output.push_back(static_cast<jchar>(code_point));
|
||||
} else {
|
||||
code_point -= 0x10000;
|
||||
output.push_back(static_cast<jchar>(0xd800 + (code_point >> 10)));
|
||||
output.push_back(static_cast<jchar>(0xdc00 + (code_point & 0x3ff)));
|
||||
}
|
||||
index += consumed;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::vector<jchar> MakeUtf16Vector(const jchar* utf16, jsize length) {
|
||||
if (utf16 != nullptr && length > 0) {
|
||||
return std::vector<jchar>(utf16, utf16 + length);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct PseudoStringObject : PseudoJavaObject {
|
||||
PseudoStringObject(std::shared_ptr<Class> cls, const char* utf)
|
||||
: PseudoJavaObject(std::move(cls)),
|
||||
chars(ModifiedUtf8ToUtf16(utf)),
|
||||
value(Utf16ToUtf8(chars)),
|
||||
modified_utf8(Utf16ToModifiedUtf8(chars)) {}
|
||||
|
||||
PseudoStringObject(std::shared_ptr<Class> cls, const jchar* utf16,
|
||||
jsize length)
|
||||
: PseudoJavaObject(std::move(cls)),
|
||||
chars(MakeUtf16Vector(utf16, length)),
|
||||
value(Utf16ToUtf8(chars)),
|
||||
modified_utf8(Utf16ToModifiedUtf8(chars)) {}
|
||||
|
||||
std::vector<jchar> chars;
|
||||
std::string value;
|
||||
std::string modified_utf8;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
jstring MakeString(const char* utf) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto cls = FallbackClassForName("java/lang/String");
|
||||
auto object = std::make_unique<PseudoStringObject>(std::move(cls), utf);
|
||||
jobject handle = StoreObject(std::move(object));
|
||||
jstring str = reinterpret_cast<jstring>(handle);
|
||||
g_known_strings.insert(str);
|
||||
return str;
|
||||
}
|
||||
|
||||
jstring MakeUtf16String(const jchar* utf16, jsize length) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto cls = FallbackClassForName("java/lang/String");
|
||||
auto object =
|
||||
std::make_unique<PseudoStringObject>(std::move(cls), utf16, length);
|
||||
jobject handle = StoreObject(std::move(object));
|
||||
jstring str = reinterpret_cast<jstring>(handle);
|
||||
g_known_strings.insert(str);
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string_view StringViewFromJString(jstring str) {
|
||||
if (__builtin_expect(!str, 0)) {
|
||||
return {};
|
||||
}
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
if (string_object) {
|
||||
return string_object->value;
|
||||
}
|
||||
const char* utf = reinterpret_cast<const char*>(str);
|
||||
return utf ? std::string_view(utf) : std::string_view();
|
||||
}
|
||||
|
||||
std::string StringFromJString(jstring str) {
|
||||
const std::string_view sv = StringViewFromJString(str);
|
||||
return std::string(sv);
|
||||
}
|
||||
|
||||
bool IsUtf8CharsetName(jstring charset_name) {
|
||||
std::string normalized = StringFromJString(charset_name);
|
||||
for (char& character : normalized) {
|
||||
if (character >= 'A' && character <= 'Z') {
|
||||
character = static_cast<char>(character - 'A' + 'a');
|
||||
}
|
||||
}
|
||||
return normalized == "utf-8" || normalized == "utf8" ||
|
||||
normalized == "unicode-1-1-utf-8";
|
||||
}
|
||||
|
||||
std::string JavaStringUtf8Bytes(const std::vector<jchar>& utf16) {
|
||||
std::string output;
|
||||
output.reserve(utf16.size());
|
||||
for (std::size_t index = 0; index < utf16.size(); ++index) {
|
||||
std::uint32_t code_point = utf16[index];
|
||||
if (code_point >= 0xd800 && code_point <= 0xdbff &&
|
||||
index + 1 < utf16.size() && utf16[index + 1] >= 0xdc00 &&
|
||||
utf16[index + 1] <= 0xdfff) {
|
||||
code_point =
|
||||
0x10000 + ((code_point - 0xd800) << 10) + (utf16[++index] - 0xdc00);
|
||||
} else if (code_point >= 0xd800 && code_point <= 0xdfff) {
|
||||
// CharsetEncoder replaces each malformed UTF-16 unit with '?'.
|
||||
output.push_back('?');
|
||||
continue;
|
||||
}
|
||||
AppendUtf8CodePoint(code_point, &output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
jbyteArray JavaStringGetUtf8Bytes(jobject obj, jstring charset_name) {
|
||||
if (!IsUtf8CharsetName(charset_name)) {
|
||||
if (TraceEnabled()) {
|
||||
std::cerr << " [JNI] java/lang/String.getBytes rejected unsupported "
|
||||
"charset\n";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string bytes;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* string_object =
|
||||
dynamic_cast<PseudoStringObject*>(PseudoObjectFromRef(obj));
|
||||
if (string_object == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
bytes = JavaStringUtf8Bytes(string_object->chars);
|
||||
}
|
||||
if (bytes.size() >
|
||||
static_cast<std::size_t>(std::numeric_limits<jsize>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jbyteArray result = MakeByteArray(static_cast<jsize>(bytes.size()));
|
||||
PseudoArray* array = ArrayFromRef(result);
|
||||
if (array == nullptr || array->bytes.size() != bytes.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!bytes.empty()) {
|
||||
std::memcpy(array->bytes.data(), bytes.data(), bytes.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static inline bool IsRawStringPointer(jstring str) {
|
||||
uintptr_t val = reinterpret_cast<uintptr_t>(str);
|
||||
if (val < 0x10000) {
|
||||
return false;
|
||||
}
|
||||
if ((val & 0xffff) == 0 && (val >> kJniHandleShift) < kJniSegmentCapacity) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* StringChars(jstring str) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (!str) {
|
||||
return nullptr;
|
||||
}
|
||||
if (g_known_strings.find(str) != g_known_strings.end()) {
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
return string_object != nullptr ? string_object->modified_utf8.c_str() : "";
|
||||
}
|
||||
if (!IsRawStringPointer(str)) {
|
||||
return "";
|
||||
}
|
||||
return reinterpret_cast<const char*>(str);
|
||||
}
|
||||
|
||||
const jchar* StringUtf16Chars(jstring str) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (!str) {
|
||||
return nullptr;
|
||||
}
|
||||
if (g_known_strings.find(str) != g_known_strings.end()) {
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
return string_object && !string_object->chars.empty()
|
||||
? string_object->chars.data()
|
||||
: kEmptyUtf16;
|
||||
}
|
||||
if (!IsRawStringPointer(str)) {
|
||||
return kEmptyUtf16;
|
||||
}
|
||||
return reinterpret_cast<const jchar*>(str);
|
||||
}
|
||||
|
||||
jsize StringUtf16Length(jstring str) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (g_known_strings.find(str) != g_known_strings.end()) {
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
return string_object != nullptr
|
||||
? static_cast<jsize>(string_object->chars.size())
|
||||
: 0;
|
||||
}
|
||||
if (!IsRawStringPointer(str)) {
|
||||
return 0;
|
||||
}
|
||||
const char* bytes = reinterpret_cast<const char*>(str);
|
||||
return bytes != nullptr ? static_cast<jsize>(std::strlen(bytes)) : 0;
|
||||
}
|
||||
|
||||
jsize StringModifiedUtf8Length(jstring str) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
if (g_known_strings.find(str) != g_known_strings.end()) {
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
return string_object != nullptr
|
||||
? static_cast<jsize>(string_object->modified_utf8.size())
|
||||
: 0;
|
||||
}
|
||||
if (!IsRawStringPointer(str)) {
|
||||
return 0;
|
||||
}
|
||||
const char* bytes = reinterpret_cast<const char*>(str);
|
||||
return bytes != nullptr ? static_cast<jsize>(std::strlen(bytes)) : 0;
|
||||
}
|
||||
|
||||
void CopyStringRegion(jstring str, jsize start, jsize length, jchar* output) {
|
||||
if (str == nullptr || output == nullptr || start < 0 || length <= 0) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
if (string_object == nullptr ||
|
||||
static_cast<std::size_t>(start) >= string_object->chars.size()) {
|
||||
return;
|
||||
}
|
||||
const std::size_t count =
|
||||
std::min(static_cast<std::size_t>(length),
|
||||
string_object->chars.size() - static_cast<std::size_t>(start));
|
||||
std::copy_n(string_object->chars.data() + start, count, output);
|
||||
}
|
||||
|
||||
void CopyStringModifiedUtf8Region(jstring str, jsize start, jsize length,
|
||||
char* output) {
|
||||
if (str == nullptr || output == nullptr || start < 0 || length <= 0) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
auto* string_object = static_cast<PseudoStringObject*>(
|
||||
PseudoObjectFromRef(reinterpret_cast<jobject>(str)));
|
||||
if (string_object == nullptr ||
|
||||
static_cast<std::size_t>(start) >= string_object->chars.size()) {
|
||||
return;
|
||||
}
|
||||
const std::size_t count =
|
||||
std::min(static_cast<std::size_t>(length),
|
||||
string_object->chars.size() - static_cast<std::size_t>(start));
|
||||
const std::string encoded = Utf16ToModifiedUtf8(
|
||||
string_object->chars, static_cast<std::size_t>(start), count);
|
||||
std::memcpy(output, encoded.data(), encoded.size());
|
||||
}
|
||||
|
||||
} // namespace jnivm::internal
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef MOCKTAIL_JNIVM_JNI_STRINGS_H_
|
||||
#define MOCKTAIL_JNIVM_JNI_STRINGS_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace jnivm::internal {
|
||||
|
||||
jstring MakeString(const char* utf);
|
||||
jstring MakeUtf16String(const jchar* utf16, jsize length);
|
||||
std::string_view StringViewFromJString(jstring str);
|
||||
std::string StringFromJString(jstring str);
|
||||
jbyteArray JavaStringGetUtf8Bytes(jobject obj, jstring charset_name);
|
||||
const char* StringChars(jstring str);
|
||||
const jchar* StringUtf16Chars(jstring str);
|
||||
jsize StringUtf16Length(jstring str);
|
||||
jsize StringModifiedUtf8Length(jstring str);
|
||||
void CopyStringRegion(jstring str, jsize start, jsize length, jchar* output);
|
||||
void CopyStringModifiedUtf8Region(jstring str, jsize start, jsize length,
|
||||
char* output);
|
||||
|
||||
} // namespace jnivm::internal
|
||||
#endif
|
||||
+67
-1042
File diff suppressed because it is too large.
Load diff
+153
-16
@@ -37,7 +37,9 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/prctl.h>
|
||||
#if defined(__x86_64__)
|
||||
#include <asm/prctl.h>
|
||||
#endif
|
||||
#include <sys/syscall.h>
|
||||
#include <vector>
|
||||
|
||||
@@ -50,6 +52,7 @@
|
||||
#include "compat/bionic_socket_runtime.h"
|
||||
#include "compat/build_profile.h"
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "compat/host_abi_experiment.h"
|
||||
#include "compat/host_abi_profile.h"
|
||||
#include "compat/host_allocator_bridge.h"
|
||||
@@ -1046,6 +1049,15 @@ void PumpRobloxMainThreadMessagesOnce() {
|
||||
mocktail::graphics::ChromeTraceWriter* trace =
|
||||
hooks.active != nullptr ? hooks.active() : nullptr;
|
||||
const std::uint64_t pump_start_ns = trace != nullptr ? hooks.clock() : 0;
|
||||
// CPU time separates a step that computes from one that waits.
|
||||
const auto thread_cpu_ns = [] {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
|
||||
return static_cast<std::uint64_t>(now.tv_sec) * 1'000'000'000ULL +
|
||||
static_cast<std::uint64_t>(now.tv_nsec);
|
||||
};
|
||||
const std::uint64_t pump_start_cpu_ns =
|
||||
trace != nullptr ? thread_cpu_ns() : 0;
|
||||
g_native_call_messages_from_main_thread(
|
||||
env, g_native_gl_class_for_main_thread);
|
||||
if (trace != nullptr) {
|
||||
@@ -1053,8 +1065,11 @@ void PumpRobloxMainThreadMessagesOnce() {
|
||||
// calls that ran a message are recorded.
|
||||
const std::uint64_t pump_end_ns = hooks.clock();
|
||||
if (pump_end_ns - pump_start_ns >= 20'000) {
|
||||
const mocktail::graphics::TraceArg args[] = {
|
||||
{"cpu_us", static_cast<std::int64_t>(
|
||||
(thread_cpu_ns() - pump_start_cpu_ns) / 1000)}};
|
||||
hooks.slice(trace, "nativeCallMessagesFromMainThread", "pump",
|
||||
pump_start_ns, pump_end_ns, nullptr, 0);
|
||||
pump_start_ns, pump_end_ns, args, 1);
|
||||
}
|
||||
}
|
||||
if (__builtin_expect(trace_pump, 0)) {
|
||||
@@ -1096,7 +1111,7 @@ struct BionicAddrInfo {
|
||||
};
|
||||
|
||||
static_assert(sizeof(BionicAddrInfo) == 48,
|
||||
"unexpected x86_64 bionic addrinfo size");
|
||||
"unexpected bionic addrinfo size");
|
||||
|
||||
bool DnsTraceEnabled() {
|
||||
return IsEnabled("MOCKTAIL_DNS_TRACE");
|
||||
@@ -1461,15 +1476,31 @@ bool IsUnsafeSoftTimeoutModule(void* rip) {
|
||||
void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) {
|
||||
static_cast<void>(info);
|
||||
auto* uc = static_cast<ucontext_t*>(context);
|
||||
#if defined(__x86_64__)
|
||||
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]) : 0;
|
||||
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RSP]) : 0;
|
||||
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]) : 0;
|
||||
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RAX]) : 0;
|
||||
const char* regs_format =
|
||||
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
|
||||
"RAX=0x%016llx\n";
|
||||
#elif defined(__aarch64__)
|
||||
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.pc) : 0;
|
||||
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.sp) : 0;
|
||||
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[29]) : 0;
|
||||
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[0]) : 0;
|
||||
const char* regs_format =
|
||||
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
|
||||
"X0=0x%016llx\n";
|
||||
#else
|
||||
uintptr_t rip = 0, rsp = 0, rbp = 0, rax = 0;
|
||||
const char* regs_format =
|
||||
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
|
||||
"X0=0x%016llx\n";
|
||||
#endif
|
||||
char regs_msg[192];
|
||||
int len = std::snprintf(
|
||||
regs_msg, sizeof(regs_msg),
|
||||
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
|
||||
"RAX=0x%016llx\n",
|
||||
regs_msg, sizeof(regs_msg), regs_format,
|
||||
static_cast<unsigned long long>(rip), static_cast<unsigned long long>(rsp),
|
||||
static_cast<unsigned long long>(rbp), static_cast<unsigned long long>(rax));
|
||||
write(2, regs_msg, static_cast<size_t>(len));
|
||||
@@ -2517,10 +2548,12 @@ jobject BuildDeviceParams(JNIEnv* env) {
|
||||
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux");
|
||||
const std::string manufacturer =
|
||||
GetEnvString("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
|
||||
const std::string device_sku =
|
||||
GetEnvString("MOCKTAIL_DEVICE_SKU", "mocktail-x86_64");
|
||||
const std::string device_sku = GetEnvString(
|
||||
"MOCKTAIL_DEVICE_SKU",
|
||||
("mocktail-" + std::string(mocktail::compat::kGuestCpuName)).c_str());
|
||||
const std::string soc_model =
|
||||
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL", "x86_64");
|
||||
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL",
|
||||
std::string(mocktail::compat::kGuestCpuName).c_str());
|
||||
SetStringField(env, params, "osVersion", "33");
|
||||
SetStringField(env, params, "deviceName", device_name.c_str());
|
||||
const std::string app_version =
|
||||
@@ -4718,12 +4751,14 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
|
||||
// return mismatches. Unsupported kernels ignore this request.
|
||||
#if defined(__x86_64__)
|
||||
{
|
||||
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
|
||||
if (r == 0) {
|
||||
std::cout << " [cet] shadow-stack (SHSTK) disabled\n";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (build_profile.allow_legacy_binary_patches) {
|
||||
std::cerr
|
||||
@@ -4916,9 +4951,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
mocktail::runtime::ReadRobloxThemeCache(
|
||||
app_storage_file, dependencies.account_identity().user_id);
|
||||
if (!saved_theme) {
|
||||
std::cerr << "[FATAL] Cannot read Roblox theme cache: "
|
||||
std::cerr << " [theme] Cannot read Roblox theme cache; using default: "
|
||||
<< saved_theme.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (saved_theme.dark_theme.has_value()) {
|
||||
dark_theme = *saved_theme.dark_theme;
|
||||
@@ -4929,7 +4963,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
if (runtime_config.theme_mode() == "roblox") {
|
||||
std::cout << " [theme] "
|
||||
<< (roblox_theme_found ? "Roblox saved theme="
|
||||
: "Roblox theme missing; default=")
|
||||
: "Roblox theme unavailable; default=")
|
||||
<< (dark_theme ? "dark" : "light") << '\n'
|
||||
<< std::flush;
|
||||
} else if (app_storage_file != nullptr) {
|
||||
@@ -4937,9 +4971,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
if (!mocktail::runtime::ApplyRobloxThemeCacheOverride(
|
||||
app_storage_file, dependencies.account_identity().user_id,
|
||||
dark_theme, &theme_error)) {
|
||||
std::cerr << "[FATAL] Roblox theme cache override failed: " << theme_error
|
||||
<< '\n';
|
||||
return EXIT_FAILURE;
|
||||
std::cerr << " [theme] Cannot persist Roblox theme override: "
|
||||
<< theme_error << '\n';
|
||||
}
|
||||
std::cout << " [theme] Roblox local theme="
|
||||
<< (dark_theme ? "dark" : "light") << '\n'
|
||||
@@ -5643,6 +5676,27 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
reinterpret_cast<void*>(mocktail_pthread_spin_trylock));
|
||||
linker::RegisterSymbol("pthread_spin_unlock",
|
||||
reinterpret_cast<void*>(mocktail_pthread_spin_unlock));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_init",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_init));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_destroy",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_destroy));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setstacksize",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setstacksize));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setdetachstate",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setdetachstate));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_setschedparam",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_setschedparam));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_getattr_np",
|
||||
reinterpret_cast<void*>(mocktail_pthread_getattr_np));
|
||||
linker::RegisterSymbol(
|
||||
"pthread_attr_getstack",
|
||||
reinterpret_cast<void*>(mocktail_pthread_attr_getstack));
|
||||
linker::RegisterSymbol("pthread_barrier_init",
|
||||
reinterpret_cast<void*>(mocktail_pthread_barrier_init));
|
||||
linker::RegisterSymbol(
|
||||
@@ -5762,7 +5816,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
if (roblox_handle == nullptr) {
|
||||
std::cerr << "\n[FATAL] Could not load '" << library_path << "'.\n"
|
||||
<< " Extract lib/x86_64/libroblox.so from a Roblox APK and\n"
|
||||
<< " Extract lib/" << compat::kGuestAbi
|
||||
<< "/libroblox.so from a Roblox APK and\n"
|
||||
<< " place it at the path above (or set ROBLOX_LIB_PATH).\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -5949,6 +6004,10 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
experience_game_symbols;
|
||||
std::shared_ptr<mocktail::runtime::RobloxExperienceComposition>
|
||||
experience_composition;
|
||||
std::unique_ptr<mocktail::runtime::RobloxPermissionsBridge>
|
||||
eager_permissions_bridge;
|
||||
std::unique_ptr<mocktail::runtime::RobloxCallProtocolBridge>
|
||||
eager_call_protocol_bridge;
|
||||
std::shared_ptr<ExperienceLifecycleTarget> experience_lifecycle_target;
|
||||
std::shared_ptr<mocktail::runtime::RobloxWindowInputRuntime>
|
||||
window_input_runtime;
|
||||
@@ -6608,6 +6667,75 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
<< " run_start_app_with_params="
|
||||
<< (run_start_app_with_params ? 1 : 0) << '\n' << std::flush;
|
||||
|
||||
// The direct GAME path still runs CoreScripts that query app protocols.
|
||||
// Install these independently of ExperienceProtocol's dynamic launch path.
|
||||
if (game_session_runtime != nullptr) {
|
||||
mocktail::runtime::JniEnvironmentProvider environment{
|
||||
raw_vm, jni_vm.get(), &RestoreGameSessionJniEnvironment};
|
||||
JNIEnv* env = nullptr;
|
||||
auto status = environment.Acquire(&env);
|
||||
if (!status.ok()) {
|
||||
std::cerr << "[FATAL] GAME protocol JNI environment unavailable\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto symbols = mocktail::runtime::ResolveRobloxPlatformWebSymbols(
|
||||
roblox_handle,
|
||||
reinterpret_cast<mocktail::runtime::SubscribeExperienceLaunchRawFn>(
|
||||
linker::ResolveSymbol(roblox_handle,
|
||||
"Java_com_roblox_universalapp_messagebus_"
|
||||
"MessageBus_doSubscribeRaw")),
|
||||
reinterpret_cast<mocktail::runtime::DeleteMessageBusConnectionFn>(
|
||||
linker::ResolveSymbol(roblox_handle,
|
||||
"Java_com_roblox_universalapp_messagebus_"
|
||||
"Connection_deleteSharedPtr")));
|
||||
jclass bus_class =
|
||||
env->FindClass("com/roblox/universalapp/messagebus/MessageBus");
|
||||
jmethodID singleton =
|
||||
bus_class ? env->GetStaticMethodID(
|
||||
bus_class, "f",
|
||||
"()Lcom/roblox/universalapp/messagebus/MessageBus;")
|
||||
: nullptr;
|
||||
jobject bus = singleton
|
||||
? env->CallStaticObjectMethod(bus_class, singleton)
|
||||
: nullptr;
|
||||
if (bus_class)
|
||||
env->DeleteLocalRef(bus_class);
|
||||
if (!bus || env->ExceptionCheck()) {
|
||||
if (env->ExceptionCheck())
|
||||
env->ExceptionClear();
|
||||
if (bus)
|
||||
env->DeleteLocalRef(bus);
|
||||
std::cerr << "[FATAL] GAME platform MessageBus is unavailable\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const mocktail::runtime::RobloxMessageBusObjects objects{
|
||||
bus,
|
||||
jni_vm.get(),
|
||||
&CreateExperienceRawCallback,
|
||||
&ClearExperienceRawCallback,
|
||||
&CreateAsyncMessageBusRequestHandler,
|
||||
&ClearAsyncMessageBusRequestHandler};
|
||||
eager_permissions_bridge =
|
||||
std::make_unique<mocktail::runtime::RobloxPermissionsBridge>(
|
||||
environment, symbols.permissions, objects,
|
||||
runtime_config.microphone_enabled());
|
||||
eager_call_protocol_bridge =
|
||||
std::make_unique<mocktail::runtime::RobloxCallProtocolBridge>(
|
||||
environment, symbols.permissions, objects);
|
||||
status = eager_permissions_bridge->Initialize();
|
||||
if (status.ok())
|
||||
status = eager_call_protocol_bridge->Initialize();
|
||||
env->DeleteLocalRef(bus);
|
||||
if (!status.ok()) {
|
||||
std::cerr << "[FATAL] GAME platform protocols did not initialize: "
|
||||
<< status.message() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << " [platform] direct GAME PermissionsProtocol and "
|
||||
"CallProtocol ready\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
if (experience_game_symbols != nullptr) {
|
||||
mocktail::runtime::RobloxExperienceMessageBusSymbols message_bus_symbols;
|
||||
message_bus_symbols.get_launch_id =
|
||||
@@ -6665,7 +6793,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
mocktail::runtime::RobloxExperienceSurfaceProvider{},
|
||||
discord_rpc.observer(),
|
||||
dependencies.clear_persisted_web_view_cookie(),
|
||||
runtime_config.microphone_enabled());
|
||||
runtime_config.microphone_enabled(),
|
||||
platform_web_symbols.account);
|
||||
const mocktail::Status platform_protocol_status =
|
||||
experience_composition->InitializePlatformProtocols();
|
||||
if (!platform_protocol_status.ok()) {
|
||||
@@ -7098,6 +7227,14 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
input_shutdown_completed = window_input_runtime->Shutdown().ok() &&
|
||||
input_shutdown_completed;
|
||||
}
|
||||
if (eager_call_protocol_bridge != nullptr) {
|
||||
(void)eager_call_protocol_bridge->Shutdown();
|
||||
eager_call_protocol_bridge.reset();
|
||||
}
|
||||
if (eager_permissions_bridge != nullptr) {
|
||||
(void)eager_permissions_bridge->Shutdown();
|
||||
eager_permissions_bridge.reset();
|
||||
}
|
||||
bool experience_destroyed_app = false;
|
||||
if (experience_composition != nullptr) {
|
||||
jni_vm->ClearRobloxExperienceLifecycleCallbacks();
|
||||
|
||||
@@ -74,16 +74,11 @@ bool ConvertSdlEvent(SDL_Window* window, const SDL_Event& source,
|
||||
SDL_GetWindowRelativeMouseMode(window)};
|
||||
return true;
|
||||
case SDL_EVENT_MOUSE_WHEEL: {
|
||||
float delta_x = source.wheel.x;
|
||||
float delta_y = source.wheel.y;
|
||||
if (source.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
delta_x = -delta_x;
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
destination->payload =
|
||||
MouseWheelEvent{delta_x, delta_y, source.wheel.mouse_x,
|
||||
source.wheel.mouse_y,
|
||||
SDL_GetWindowRelativeMouseMode(window)};
|
||||
// SDL already applies the host's natural-scrolling preference to x/y.
|
||||
// Undoing SDL_MOUSEWHEEL_FLIPPED here would ignore the OS setting.
|
||||
destination->payload = MouseWheelEvent{
|
||||
source.wheel.x, source.wheel.y, source.wheel.mouse_x,
|
||||
source.wheel.mouse_y, SDL_GetWindowRelativeMouseMode(window)};
|
||||
return true;
|
||||
}
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
#include "runtime/roblox_launch_uri.h"
|
||||
|
||||
namespace mocktail {
|
||||
@@ -262,11 +263,12 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
usage
|
||||
<< "Usage: " << (program_name.empty() ? "mocktail" : program_name)
|
||||
<< " [options] [roblox-uri]\n\n"
|
||||
<< "Normal startup downloads and activates a verified managed x86_64 "
|
||||
"Roblox payload automatically.\n\n"
|
||||
<< "Normal startup downloads and activates a verified managed "
|
||||
<< compat::kGuestAbi
|
||||
<< " Roblox payload automatically.\n\n"
|
||||
<< "Options:\n"
|
||||
<< " --roblox-lib <path> Development override for a specific "
|
||||
"x86_64 libroblox.so\n"
|
||||
<< compat::kGuestAbi << " libroblox.so\n"
|
||||
<< " --headless Run without creating an SDL window\n"
|
||||
<< " --windowed Force windowed startup (default)\n"
|
||||
<< " --graphics <backend> direct-vulkan | opengl | system | "
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "runtime/engine_pump_rest.h"
|
||||
|
||||
#include <cpuid.h>
|
||||
#include <immintrin.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <time.h>
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#include <cpuid.h>
|
||||
#include <immintrin.h>
|
||||
#include <x86intrin.h>
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -23,11 +26,20 @@ uint64_t NowNs() {
|
||||
// The kernel caps one TPAUSE at umwait_control/max_time TSC cycles (100000 by
|
||||
// default), so the wait is a loop of short pauses up to the deadline. Control
|
||||
// bit 0 clear selects C0.2, the deeper of the two light states.
|
||||
#if defined(__x86_64__)
|
||||
__attribute__((target("waitpkg"))) void TpauseUntil(uint64_t deadline_ns) {
|
||||
while (NowNs() < deadline_ns) {
|
||||
_tpause(0, __rdtsc() + 100000ULL);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// TPAUSE is x86-only. CpuHasWaitPkg() is false on other hosts, so kTpause is
|
||||
// never chosen there; this waits out the deadline if it is forced anyway.
|
||||
void TpauseUntil(uint64_t deadline_ns) {
|
||||
while (NowNs() < deadline_ns) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void LowerTimerSlackOnce() {
|
||||
// Only the main thread rests, so the slack is set once per process.
|
||||
@@ -88,11 +100,15 @@ bool CpuGovernorIsPerformance(const char* path) {
|
||||
}
|
||||
|
||||
bool CpuHasWaitPkg() {
|
||||
#if defined(__x86_64__)
|
||||
unsigned eax = 0, ebx = 0, ecx = 0, edx = 0;
|
||||
if (__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx) == 0) {
|
||||
return false;
|
||||
}
|
||||
return (ecx & (1u << 5)) != 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
EnginePumpRestMode ActiveEnginePumpRestMode() {
|
||||
|
||||
@@ -29,12 +29,21 @@ constexpr const char* kIcdDirectories[] = {
|
||||
"/usr/local/share/vulkan/icd.d",
|
||||
};
|
||||
|
||||
#if defined(__aarch64__)
|
||||
constexpr char kNativeArchitecture[] = "aarch64";
|
||||
|
||||
constexpr const char* kForeignArchitectures[] = {
|
||||
"i686", "i586", "i486", "i386", "x86_64", "x86.",
|
||||
"amd64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
|
||||
};
|
||||
#else
|
||||
constexpr char kNativeArchitecture[] = "x86_64";
|
||||
|
||||
constexpr const char* kForeignArchitectures[] = {
|
||||
"i686", "i586", "i486", "i386", "x86.", "aarch64",
|
||||
"arm64", "armhf", "armv7", "ppc64", "riscv64", "s390x",
|
||||
};
|
||||
#endif
|
||||
|
||||
struct HostGpus {
|
||||
bool intel = false;
|
||||
|
||||
@@ -359,46 +359,43 @@ RobloxThemeCacheResult ReadRobloxThemeCache(
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto read_theme = [&result](const nlohmann::json& value,
|
||||
std::string_view name) {
|
||||
const auto read_theme = [](const nlohmann::json& value) -> std::optional<bool> {
|
||||
if (!value.is_string()) {
|
||||
result.error = std::string(name) + " is not a string";
|
||||
return;
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string& theme = value.get_ref<const std::string&>();
|
||||
if (theme != "dark" && theme != "light") {
|
||||
result.error = std::string(name) + " must be dark or light";
|
||||
return;
|
||||
if (theme == "dark") {
|
||||
return true;
|
||||
}
|
||||
result.dark_theme = theme == "dark";
|
||||
if (theme == "light") {
|
||||
return false;
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (authenticated_user_id >= 0) {
|
||||
const auto encoded_device_themes = storage.value.find("DeviceLevelTheme");
|
||||
if (encoded_device_themes != storage.value.end()) {
|
||||
if (!encoded_device_themes->is_string()) {
|
||||
result.error = "Roblox DeviceLevelTheme is not an encoded object";
|
||||
return result;
|
||||
}
|
||||
if (encoded_device_themes != storage.value.end() &&
|
||||
encoded_device_themes->is_string()) {
|
||||
const nlohmann::json device_themes = nlohmann::json::parse(
|
||||
encoded_device_themes->get_ref<const std::string&>(), nullptr, false,
|
||||
true);
|
||||
if (device_themes.is_discarded() || !device_themes.is_object()) {
|
||||
result.error = "Roblox DeviceLevelTheme encoding is invalid";
|
||||
return result;
|
||||
}
|
||||
const auto account_theme =
|
||||
device_themes.find(std::to_string(authenticated_user_id));
|
||||
if (account_theme != device_themes.end()) {
|
||||
read_theme(*account_theme, "Roblox account theme");
|
||||
return result;
|
||||
if (device_themes.is_object()) {
|
||||
const auto account_theme =
|
||||
device_themes.find(std::to_string(authenticated_user_id));
|
||||
if (account_theme != device_themes.end()) {
|
||||
result.dark_theme = read_theme(*account_theme);
|
||||
if (result.dark_theme.has_value()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto authenticated_theme = storage.value.find("AuthenticatedTheme");
|
||||
if (authenticated_theme != storage.value.end()) {
|
||||
read_theme(*authenticated_theme, "Roblox authenticated theme");
|
||||
result.dark_theme = read_theme(*authenticated_theme);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "runtime/roblox_account_bridge.h"
|
||||
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kAccountProtocolClass[] =
|
||||
"com/roblox/universalapp/account/JNIAccountProtocol";
|
||||
// Serialized name of the APK status enum for a missing Play token provider.
|
||||
constexpr char kTokenProviderUninitialized[] = "TOKEN_PROVIDER_UNINITIALIZED";
|
||||
constexpr std::size_t kMaximumNameBytes = 128;
|
||||
|
||||
enum Method : std::size_t { kDeviceIntegrityAvailable, kGetIntegrityToken };
|
||||
constexpr std::size_t kMethodCount = 2;
|
||||
constexpr const char* kMethodLabels[kMethodCount] = {"DeviceIntegrityAvailable",
|
||||
"GetIntegrityToken"};
|
||||
|
||||
Status Unavailable(const char* message) {
|
||||
return Status::Error(StatusCode::kUnavailable, message);
|
||||
}
|
||||
|
||||
Status CheckJni(JNIEnv* env) {
|
||||
if (!env->ExceptionCheck()) return Status::Ok();
|
||||
env->ExceptionClear();
|
||||
return Unavailable("AccountProtocol JNI operation failed");
|
||||
}
|
||||
|
||||
// Copies a non-empty, bounded engine-provided name.
|
||||
bool ReadName(JNIEnv* env, jstring value, std::string* name) {
|
||||
if (!value) return false;
|
||||
const jsize size = env->GetStringUTFLength(value);
|
||||
const char* bytes = env->GetStringUTFChars(value, nullptr);
|
||||
const bool valid = bytes && size > 0 &&
|
||||
static_cast<std::size_t>(size) <= kMaximumNameBytes;
|
||||
if (valid) name->assign(bytes, static_cast<std::size_t>(size));
|
||||
if (bytes) env->ReleaseStringUTFChars(value, bytes);
|
||||
return valid;
|
||||
}
|
||||
|
||||
// Pins a valid engine-provided name as a global reference.
|
||||
jstring RetainName(JNIEnv* env, jstring value) {
|
||||
std::string ignored;
|
||||
return ReadName(env, value, &ignored)
|
||||
? static_cast<jstring>(env->NewGlobalRef(value))
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct RobloxAccountBridge::State {
|
||||
struct Endpoint {
|
||||
std::shared_ptr<State> state;
|
||||
Method method;
|
||||
};
|
||||
struct Registration {
|
||||
jobject handler = nullptr;
|
||||
jstring method = nullptr;
|
||||
bool registered = false;
|
||||
};
|
||||
|
||||
jobject bus = nullptr;
|
||||
jstring protocol = nullptr;
|
||||
std::string support_key;
|
||||
std::string token_key;
|
||||
std::string result_key;
|
||||
std::array<Registration, kMethodCount> registrations;
|
||||
std::mutex mutex;
|
||||
std::condition_variable drained;
|
||||
bool accepting = true;
|
||||
std::size_t in_flight = 0;
|
||||
|
||||
// The request payload is never parsed or logged: both answers are fixed.
|
||||
static std::string Answer(void* context, JNIEnv*, jstring) {
|
||||
const auto* endpoint = static_cast<Endpoint*>(context);
|
||||
const auto state = endpoint->state;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (!state->accepting) return {};
|
||||
++state->in_flight;
|
||||
}
|
||||
struct Finish {
|
||||
std::shared_ptr<State> state;
|
||||
~Finish() {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
--state->in_flight;
|
||||
state->drained.notify_all();
|
||||
}
|
||||
} finish{state};
|
||||
|
||||
nlohmann::json body;
|
||||
const char* result = nullptr;
|
||||
if (endpoint->method == kDeviceIntegrityAvailable) {
|
||||
body = {{state->support_key, false}};
|
||||
result = "unsupported";
|
||||
} else {
|
||||
body = {{state->token_key, ""},
|
||||
{state->result_key, kTokenProviderUninitialized}};
|
||||
result = kTokenProviderUninitialized;
|
||||
}
|
||||
std::fprintf(stderr, " [account] AccountProtocol method=%s result=%s\n",
|
||||
kMethodLabels[endpoint->method], result);
|
||||
return body.dump();
|
||||
}
|
||||
};
|
||||
|
||||
RobloxAccountBridge::RobloxAccountBridge(JniEnvironmentProvider environment,
|
||||
RobloxAccountProtocolSymbols symbols,
|
||||
RobloxAccountProtocolObjects objects)
|
||||
: environment_(environment), symbols_(symbols), objects_(objects) {}
|
||||
|
||||
RobloxAccountBridge::~RobloxAccountBridge() { (void)Shutdown(); }
|
||||
|
||||
Status RobloxAccountBridge::Initialize() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
if (state_) {
|
||||
return Status::Error(StatusCode::kFailedPrecondition,
|
||||
"AccountProtocol is already initialized");
|
||||
}
|
||||
if (!environment_.valid() || !symbols_.complete() || !objects_.complete()) {
|
||||
return Unavailable("AccountProtocol prerequisites are incomplete");
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
|
||||
jclass protocol_class = env->FindClass(kAccountProtocolClass);
|
||||
if (!protocol_class) {
|
||||
(void)CheckJni(env);
|
||||
return Unavailable("JNIAccountProtocol class is unavailable");
|
||||
}
|
||||
const jstring protocol = symbols_.get_protocol_name(env, protocol_class);
|
||||
const jstring methods[kMethodCount] = {
|
||||
symbols_.get_device_integrity_available_method_name(env, protocol_class),
|
||||
symbols_.get_integrity_token_method_name(env, protocol_class)};
|
||||
const jstring keys[] = {symbols_.get_support_key(env, protocol_class),
|
||||
symbols_.get_token_key(env, protocol_class),
|
||||
symbols_.get_result_key(env, protocol_class)};
|
||||
env->DeleteLocalRef(protocol_class);
|
||||
|
||||
state_ = std::make_shared<State>();
|
||||
std::string* const key_targets[] = {&state_->support_key,
|
||||
&state_->token_key, &state_->result_key};
|
||||
bool names_valid = true;
|
||||
for (std::size_t i = 0; i < std::size(keys); ++i) {
|
||||
names_valid = ReadName(env, keys[i], key_targets[i]) && names_valid;
|
||||
if (keys[i]) env->DeleteLocalRef(keys[i]);
|
||||
}
|
||||
state_->protocol = RetainName(env, protocol);
|
||||
if (protocol) env->DeleteLocalRef(protocol);
|
||||
for (std::size_t i = 0; i < kMethodCount; ++i) {
|
||||
state_->registrations[i].method = RetainName(env, methods[i]);
|
||||
names_valid = state_->registrations[i].method && names_valid;
|
||||
if (methods[i]) env->DeleteLocalRef(methods[i]);
|
||||
}
|
||||
state_->bus = env->NewGlobalRef(objects_.message_bus);
|
||||
auto fail = [this](Status failure) {
|
||||
(void)ShutdownLocked();
|
||||
return failure;
|
||||
};
|
||||
if (!CheckJni(env).ok() || !names_valid || !state_->protocol ||
|
||||
!state_->bus) {
|
||||
return fail(Unavailable("AccountProtocol names are unavailable"));
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < kMethodCount; ++i) {
|
||||
auto& entry = state_->registrations[i];
|
||||
auto endpoint = std::make_shared<State::Endpoint>(
|
||||
State::Endpoint{state_, static_cast<Method>(i)});
|
||||
jobject handler = objects_.create_request_handler(objects_.context,
|
||||
endpoint, &State::Answer);
|
||||
if (handler) {
|
||||
entry.handler = env->NewGlobalRef(handler);
|
||||
if (!entry.handler) {
|
||||
objects_.clear_request_handler(objects_.context, handler);
|
||||
}
|
||||
env->DeleteLocalRef(handler);
|
||||
}
|
||||
if (!entry.handler || !CheckJni(env).ok()) {
|
||||
return fail(Unavailable("could not create AccountProtocol handler"));
|
||||
}
|
||||
entry.registered = true;
|
||||
symbols_.set_request_handler_raw(env, state_->bus, state_->protocol,
|
||||
entry.method, entry.handler);
|
||||
status = CheckJni(env);
|
||||
if (!status.ok()) return fail(status);
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [account] AccountProtocol ready device_integrity=unsupported\n");
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxAccountBridge::Shutdown() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
return ShutdownLocked();
|
||||
}
|
||||
|
||||
Status RobloxAccountBridge::ShutdownLocked() {
|
||||
if (!state_) return Status::Ok();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_->mutex);
|
||||
state_->accepting = false;
|
||||
state_->drained.wait(lock, [this] { return state_->in_flight == 0; });
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status; // Keep roots for a later shutdown retry.
|
||||
// Clear any exception from partially failed initialization before cleanup.
|
||||
const Status pending = CheckJni(env);
|
||||
if (!pending.ok()) status = pending;
|
||||
for (auto& entry : state_->registrations) {
|
||||
if (entry.registered) {
|
||||
symbols_.clear_request_handler(env, state_->bus, state_->protocol,
|
||||
entry.method);
|
||||
const Status cleared = CheckJni(env);
|
||||
if (!cleared.ok()) status = cleared;
|
||||
}
|
||||
if (entry.handler) {
|
||||
objects_.clear_request_handler(objects_.context, entry.handler);
|
||||
env->DeleteGlobalRef(entry.handler);
|
||||
}
|
||||
if (entry.method) env->DeleteGlobalRef(entry.method);
|
||||
entry = {};
|
||||
}
|
||||
if (state_->protocol) env->DeleteGlobalRef(state_->protocol);
|
||||
if (state_->bus) env->DeleteGlobalRef(state_->bus);
|
||||
state_.reset();
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "runtime/roblox_call_protocol_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
RobloxCallProtocolBridge::RobloxCallProtocolBridge(
|
||||
JniEnvironmentProvider environment, RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects)
|
||||
: bridge_(environment, symbols, objects, "Call", {"getCallState"},
|
||||
[](JNIEnv* env, jstring request, std::size_t) {
|
||||
// CallProtocolCore registers the wire protocol "Call" (not the
|
||||
// Lua module name "CallProtocol"). Its handler ignores the
|
||||
// payload and returns CallState::serializeCallStateJson. Match
|
||||
// the no-call state; preserve the caller's default-muted
|
||||
// initialization path.
|
||||
if (request && env->GetStringUTFLength(request) > 65536) {
|
||||
return RobloxMessageBusReply{"{}", 13, "oversized_request"};
|
||||
}
|
||||
return RobloxMessageBusReply{
|
||||
R"({"status":"Idle","muted":true,"camEnabled":false})", 0,
|
||||
"idle"};
|
||||
}) {}
|
||||
|
||||
Status RobloxCallProtocolBridge::Initialize() {
|
||||
return bridge_.Initialize();
|
||||
}
|
||||
|
||||
Status RobloxCallProtocolBridge::Shutdown() {
|
||||
return bridge_.Shutdown();
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
#include "runtime/external_launch_broker.h"
|
||||
#include "runtime/roblox_launch_uri.h"
|
||||
#include "runtime/webview_helper_launcher.h"
|
||||
#include "window/window.h"
|
||||
|
||||
@@ -21,7 +22,13 @@ namespace {
|
||||
constexpr size_t kMaxPendingLaunchRequests = 8;
|
||||
// libroblox's static TLS requires the proven 16 MiB guest-thread stack floor.
|
||||
constexpr size_t kLaunchWorkerStackSize = 64ULL * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{3000};
|
||||
// Covers helper exec through window creation, including cold Flatpak starts.
|
||||
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{5000};
|
||||
// Roblox closes a solved WebViewProtocol challenge itself with closeWindow,
|
||||
// as on Android. The host closes it only if Roblox has not done so within
|
||||
// this period. Captcha windows close at once instead (see
|
||||
// RobloxWebViewOpenRequest::close_when_challenge_solved).
|
||||
constexpr std::chrono::milliseconds kSolvedChallengeCloseGrace{3000};
|
||||
constexpr char kRobloxBaseUrl[] = "https://www.roblox.com/";
|
||||
|
||||
Status Invalid(std::string message) {
|
||||
@@ -131,11 +138,16 @@ Status PublishSystemTheme(JNIEnv* env,
|
||||
status = CheckJni(env, "publish SystemTheme.systemThemeUpdated");
|
||||
}
|
||||
|
||||
if (message_id != nullptr) env->DeleteLocalRef(message_id);
|
||||
if (payload != nullptr) env->DeleteLocalRef(payload);
|
||||
if (message != nullptr) env->DeleteLocalRef(message);
|
||||
if (protocol != nullptr) env->DeleteLocalRef(protocol);
|
||||
if (message_bus_class != nullptr) env->DeleteLocalRef(message_bus_class);
|
||||
if (message_id != nullptr)
|
||||
env->DeleteLocalRef(message_id);
|
||||
if (payload != nullptr)
|
||||
env->DeleteLocalRef(payload);
|
||||
if (message != nullptr)
|
||||
env->DeleteLocalRef(message);
|
||||
if (protocol != nullptr)
|
||||
env->DeleteLocalRef(protocol);
|
||||
if (message_bus_class != nullptr)
|
||||
env->DeleteLocalRef(message_bus_class);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -158,24 +170,33 @@ Status LaunchRobloxWebSurface(
|
||||
}
|
||||
std::fprintf(stderr, " [%s] opening validated Roblox web surface\n",
|
||||
transport);
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
const WebViewHelperLaunchResult launched =
|
||||
LaunchWebViewHelper(helper, url, std::move(exit_observer));
|
||||
if (!launched) {
|
||||
return Unavailable("could not display Roblox web surface: " +
|
||||
launched.error);
|
||||
}
|
||||
if (launched.process == nullptr ||
|
||||
!launched.process->WaitUntilReady(kWebSurfaceReadyTimeout)) {
|
||||
const bool ready = launched.process != nullptr &&
|
||||
launched.process->WaitUntilReady(kWebSurfaceReadyTimeout);
|
||||
const long long elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - started)
|
||||
.count();
|
||||
if (!ready) {
|
||||
if (launched.process != nullptr) {
|
||||
(void)launched.process->RequestClose();
|
||||
}
|
||||
return Unavailable(
|
||||
"Roblox web surface did not become ready before the deadline");
|
||||
"Roblox web surface did not become ready before the deadline (" +
|
||||
std::to_string(elapsed_ms) + " ms of " +
|
||||
std::to_string(kWebSurfaceReadyTimeout.count()) + " ms)");
|
||||
}
|
||||
if (process != nullptr) {
|
||||
*process = launched.process;
|
||||
}
|
||||
std::fprintf(stderr, " [%s] Roblox web surface launched\n", transport);
|
||||
std::fprintf(stderr, " [%s] Roblox web surface launched in %lld ms\n",
|
||||
transport, elapsed_ms);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
@@ -187,7 +208,8 @@ template <>
|
||||
bool SetField<jint>(JNIEnv* env, jobject object, jclass clazz, const char* name,
|
||||
const char* signature, jint value) {
|
||||
jfieldID field = env->GetFieldID(clazz, name, signature);
|
||||
if (field == nullptr) return false;
|
||||
if (field == nullptr)
|
||||
return false;
|
||||
env->SetIntField(object, field, value);
|
||||
return true;
|
||||
}
|
||||
@@ -196,7 +218,8 @@ template <>
|
||||
bool SetField<jlong>(JNIEnv* env, jobject object, jclass clazz,
|
||||
const char* name, const char* signature, jlong value) {
|
||||
jfieldID field = env->GetFieldID(clazz, name, signature);
|
||||
if (field == nullptr) return false;
|
||||
if (field == nullptr)
|
||||
return false;
|
||||
env->SetLongField(object, field, value);
|
||||
return true;
|
||||
}
|
||||
@@ -206,7 +229,8 @@ bool SetField<jboolean>(JNIEnv* env, jobject object, jclass clazz,
|
||||
const char* name, const char* signature,
|
||||
jboolean value) {
|
||||
jfieldID field = env->GetFieldID(clazz, name, signature);
|
||||
if (field == nullptr) return false;
|
||||
if (field == nullptr)
|
||||
return false;
|
||||
env->SetBooleanField(object, field, value);
|
||||
return true;
|
||||
}
|
||||
@@ -215,7 +239,8 @@ template <>
|
||||
bool SetField<jfloat>(JNIEnv* env, jobject object, jclass clazz,
|
||||
const char* name, const char* signature, jfloat value) {
|
||||
jfieldID field = env->GetFieldID(clazz, name, signature);
|
||||
if (field == nullptr) return false;
|
||||
if (field == nullptr)
|
||||
return false;
|
||||
env->SetFloatField(object, field, value);
|
||||
return true;
|
||||
}
|
||||
@@ -224,7 +249,8 @@ template <>
|
||||
bool SetField<jobject>(JNIEnv* env, jobject object, jclass clazz,
|
||||
const char* name, const char* signature, jobject value) {
|
||||
jfieldID field = env->GetFieldID(clazz, name, signature);
|
||||
if (field == nullptr) return false;
|
||||
if (field == nullptr)
|
||||
return false;
|
||||
env->SetObjectField(object, field, value);
|
||||
return true;
|
||||
}
|
||||
@@ -299,12 +325,14 @@ RobloxExperienceComposition::RobloxExperienceComposition(
|
||||
const SecureRobloxCredential* initial_web_view_credential,
|
||||
RobloxExperienceSurfaceProvider surface_provider,
|
||||
RobloxExperiencePresenceObserver presence_observer,
|
||||
bool clear_persisted_web_view_cookie, bool microphone_enabled)
|
||||
bool clear_persisted_web_view_cookie, bool microphone_enabled,
|
||||
RobloxAccountProtocolSymbols account_symbols)
|
||||
: environment_(environment),
|
||||
message_bus_symbols_(message_bus_symbols),
|
||||
web_view_symbols_(web_view_symbols),
|
||||
browser_service_symbols_(browser_service_symbols),
|
||||
permissions_symbols_(permissions_symbols),
|
||||
account_symbols_(account_symbols),
|
||||
microphone_enabled_(microphone_enabled),
|
||||
game_symbols_(game_symbols),
|
||||
jni_factory_(jni_factory),
|
||||
@@ -334,7 +362,9 @@ RobloxExperienceComposition::RobloxExperienceComposition(
|
||||
}
|
||||
}
|
||||
|
||||
RobloxExperienceComposition::~RobloxExperienceComposition() { Shutdown(); }
|
||||
RobloxExperienceComposition::~RobloxExperienceComposition() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::InitializePlatformProtocols() {
|
||||
if (!web_view_cookie_initialization_error_.empty()) {
|
||||
@@ -348,7 +378,8 @@ Status RobloxExperienceComposition::InitializePlatformProtocols() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (platform_protocols_initialized_ || objects_ != nullptr ||
|
||||
web_view_bridge_ != nullptr || permissions_bridge_ != nullptr) {
|
||||
web_view_bridge_ != nullptr || permissions_bridge_ != nullptr ||
|
||||
call_protocol_bridge_ != nullptr) {
|
||||
return FailedPrecondition("platform protocols are already initialized");
|
||||
}
|
||||
}
|
||||
@@ -425,16 +456,49 @@ Status RobloxExperienceComposition::InitializePlatformProtocols() {
|
||||
(void)ReleaseGlobalObjects();
|
||||
return status;
|
||||
}
|
||||
auto call_protocol_bridge = std::make_unique<RobloxCallProtocolBridge>(
|
||||
environment_, permissions_symbols_, permissions_objects);
|
||||
status = call_protocol_bridge->Initialize();
|
||||
if (!status.ok()) {
|
||||
(void)permissions_bridge->Shutdown();
|
||||
(void)browser_service_bridge->Shutdown();
|
||||
(void)web_view_bridge->Shutdown();
|
||||
(void)ReleaseGlobalObjects();
|
||||
return status;
|
||||
}
|
||||
// AccountProtocol only shortens a device-integrity login challenge that
|
||||
// cannot succeed here, so its absence or failure never blocks startup.
|
||||
std::unique_ptr<RobloxAccountBridge> account_bridge;
|
||||
if (account_symbols_.complete()) {
|
||||
account_bridge = std::make_unique<RobloxAccountBridge>(
|
||||
environment_, account_symbols_,
|
||||
RobloxAccountProtocolObjects{web_view_objects.message_bus,
|
||||
jni_factory_.context,
|
||||
jni_factory_.create_request_handler,
|
||||
jni_factory_.clear_request_handler});
|
||||
const Status account_status = account_bridge->Initialize();
|
||||
if (!account_status.ok()) {
|
||||
std::fprintf(stderr, " [account] AccountProtocol unavailable: %s\n",
|
||||
account_status.message().c_str());
|
||||
account_bridge.reset();
|
||||
}
|
||||
} else {
|
||||
std::fprintf(stderr,
|
||||
" [account] AccountProtocol entrypoints are missing; "
|
||||
"device-integrity challenges wait for Roblox's timeout\n");
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
web_view_bridge_ = std::move(web_view_bridge);
|
||||
browser_service_bridge_ = std::move(browser_service_bridge);
|
||||
permissions_bridge_ = std::move(permissions_bridge);
|
||||
call_protocol_bridge_ = std::move(call_protocol_bridge);
|
||||
account_bridge_ = std::move(account_bridge);
|
||||
platform_protocols_initialized_ = true;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [platform] Android WebViewProtocol, BrowserService and "
|
||||
"PermissionsProtocol "
|
||||
"PermissionsProtocol and CallProtocol "
|
||||
"bridges ready\n");
|
||||
return Status::Ok();
|
||||
}
|
||||
@@ -454,7 +518,8 @@ Status RobloxExperienceComposition::OnLuaAppReady(
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!platform_protocols_initialized_ || objects_ == nullptr ||
|
||||
objects_->message_bus == nullptr || web_view_bridge_ == nullptr ||
|
||||
browser_service_bridge_ == nullptr || permissions_bridge_ == nullptr) {
|
||||
browser_service_bridge_ == nullptr || permissions_bridge_ == nullptr ||
|
||||
call_protocol_bridge_ == nullptr) {
|
||||
return FailedPrecondition(
|
||||
"platform protocols must be initialized before LuaApp readiness");
|
||||
}
|
||||
@@ -509,7 +574,8 @@ Status RobloxExperienceComposition::OnLuaAppReady(
|
||||
controller_.reset();
|
||||
}
|
||||
}
|
||||
if (!status.ok()) (void)ReleaseLuaAppGlobalObjects();
|
||||
if (!status.ok())
|
||||
(void)ReleaseLuaAppGlobalObjects();
|
||||
if (status.ok()) {
|
||||
std::fprintf(stderr,
|
||||
" [theme] published SystemTheme.systemThemeUpdated (%s)\n",
|
||||
@@ -582,8 +648,7 @@ Status RobloxExperienceComposition::OpenWebSurface(
|
||||
!current->SetBackNavigationDisabled(
|
||||
presentation.back_navigation_disabled) ||
|
||||
!current->SetShowDomainAsTitle(presentation.show_domain_as_title) ||
|
||||
!cookie_synchronized ||
|
||||
(!spawn && !current->LoadUrl(url))) {
|
||||
!cookie_synchronized || (!spawn && !current->LoadUrl(url))) {
|
||||
(void)current->RequestClose();
|
||||
return Unavailable("could not configure reusable Roblox web surface");
|
||||
}
|
||||
@@ -602,6 +667,8 @@ Status RobloxExperienceComposition::OpenWebSurface(
|
||||
web_surface_process_generation_ = process_generation;
|
||||
web_surface_route_ = route;
|
||||
web_surface_logical_generation_ = logical_generation;
|
||||
web_surface_close_when_challenge_solved_ =
|
||||
presentation.close_when_challenge_solved;
|
||||
web_surface_logical_exit_observer_ = std::move(exit_observer);
|
||||
}
|
||||
if (superseded_observer.valid()) {
|
||||
@@ -614,13 +681,16 @@ Status RobloxExperienceComposition::OpenWebSurface(
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::CloseWebSurface() {
|
||||
Status RobloxExperienceComposition::CloseWebSurface(
|
||||
uint64_t expected_logical_generation) {
|
||||
std::lock_guard<std::mutex> operation_lock(web_surface_operation_mutex_);
|
||||
std::shared_ptr<WebViewHelperProcess> process;
|
||||
WebViewHelperExitObserver exit_observer;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (web_surface_route_ == WebSurfaceRoute::kNone) {
|
||||
if (web_surface_route_ == WebSurfaceRoute::kNone ||
|
||||
(expected_logical_generation != 0 &&
|
||||
expected_logical_generation != web_surface_logical_generation_)) {
|
||||
return Status::Ok();
|
||||
}
|
||||
process = std::move(web_surface_process_);
|
||||
@@ -628,6 +698,7 @@ Status RobloxExperienceComposition::CloseWebSurface() {
|
||||
exit_observer = std::move(web_surface_logical_exit_observer_);
|
||||
web_surface_route_ = WebSurfaceRoute::kNone;
|
||||
web_surface_logical_generation_ = 0;
|
||||
web_surface_close_when_challenge_solved_ = false;
|
||||
}
|
||||
// A closed challenge must stop executing. Reusing its hidden document lets
|
||||
// late callbacks from the old attempt reach the next login attempt. The APK
|
||||
@@ -643,6 +714,33 @@ Status RobloxExperienceComposition::CloseWebSurface() {
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::CloseUnclaimedSolvedChallenge() {
|
||||
uint64_t generation = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!solved_challenge_close_deadline_.has_value()) {
|
||||
return Status::Ok();
|
||||
}
|
||||
const bool still_open =
|
||||
web_surface_route_ != WebSurfaceRoute::kNone &&
|
||||
solved_challenge_logical_generation_ == web_surface_logical_generation_;
|
||||
if (still_open &&
|
||||
std::chrono::steady_clock::now() < *solved_challenge_close_deadline_) {
|
||||
return Status::Ok();
|
||||
}
|
||||
solved_challenge_close_deadline_.reset();
|
||||
if (!still_open) {
|
||||
return Status::Ok();
|
||||
}
|
||||
generation = solved_challenge_logical_generation_;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [webview] closing solved challenge window; Roblox did not "
|
||||
"close it within %lld ms\n",
|
||||
static_cast<long long>(kSolvedChallengeCloseGrace.count()));
|
||||
return CloseWebSurface(generation);
|
||||
}
|
||||
|
||||
void RobloxExperienceComposition::WebSurfaceExited(void* context) {
|
||||
auto* exit_context = static_cast<WebSurfaceExitContext*>(context);
|
||||
if (exit_context == nullptr || exit_context->target == nullptr ||
|
||||
@@ -668,6 +766,7 @@ void RobloxExperienceComposition::HandleWebSurfaceExit(
|
||||
web_surface_process_generation_ = 0;
|
||||
web_surface_route_ = WebSurfaceRoute::kNone;
|
||||
web_surface_logical_generation_ = 0;
|
||||
web_surface_close_when_challenge_solved_ = false;
|
||||
exit_observer = std::move(web_surface_logical_exit_observer_);
|
||||
}
|
||||
if (exit_observer.valid()) {
|
||||
@@ -693,6 +792,8 @@ Status RobloxExperienceComposition::DispatchWebViewOpen(
|
||||
// host back navigation too.
|
||||
presentation.back_navigation_disabled =
|
||||
request.back_button_visible.has_value() && !*request.back_button_visible;
|
||||
presentation.close_when_challenge_solved =
|
||||
request.close_when_challenge_solved;
|
||||
return composition->OpenWebSurface(request.url, "webview",
|
||||
WebSurfaceRoute::kWebView,
|
||||
std::move(exit_observer), presentation);
|
||||
@@ -951,9 +1052,45 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
|
||||
route = web_surface_route_;
|
||||
web_view_bridge = web_view_bridge_.get();
|
||||
}
|
||||
return event.type == WebViewHelperEventType::kRobloxCookie
|
||||
? AcceptWebViewRobloxCookie(event.payload)
|
||||
: RouteWebSurfaceEvent(route, event, web_view_bridge);
|
||||
if (event.type == WebViewHelperEventType::kRobloxCookie) {
|
||||
return AcceptWebViewRobloxCookie(event.payload);
|
||||
}
|
||||
if (event.type == WebViewHelperEventType::kExecuteRoblox) {
|
||||
// A web Play button's launchGame is launched through the experience
|
||||
// pipeline and closes the surface. It is never forwarded, so Roblox
|
||||
// cannot start a second join from the same command.
|
||||
RobloxExperienceLaunchRequest request;
|
||||
if (ParseRobloxWebViewLaunchGame(event.payload, &request).ok() &&
|
||||
Dispatch(request).ok()) {
|
||||
std::fprintf(stderr,
|
||||
"[experience] WebView launchGame queued place_id=%lld\n",
|
||||
static_cast<long long>(request.place_id));
|
||||
return CloseWebSurface();
|
||||
}
|
||||
}
|
||||
const Status status = RouteWebSurfaceEvent(route, event, web_view_bridge);
|
||||
if (!status.ok() || !IsRobloxWebViewChallengeSuccess(event.payload)) {
|
||||
return status;
|
||||
}
|
||||
std::fprintf(stderr, " [webview] solved challenge forwarded to Roblox\n");
|
||||
bool close_now = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (logical_generation != web_surface_logical_generation_) {
|
||||
return status;
|
||||
}
|
||||
close_now = web_surface_close_when_challenge_solved_;
|
||||
if (!close_now) {
|
||||
solved_challenge_close_deadline_ =
|
||||
std::chrono::steady_clock::now() + kSolvedChallengeCloseGrace;
|
||||
solved_challenge_logical_generation_ = logical_generation;
|
||||
}
|
||||
}
|
||||
if (!close_now) {
|
||||
return status;
|
||||
}
|
||||
std::fprintf(stderr, " [webview] closing solved captcha window\n");
|
||||
return CloseWebSurface(logical_generation);
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::DrainPlatformEvents() {
|
||||
@@ -1002,6 +1139,9 @@ Status RobloxExperienceComposition::DrainPlatformEvents() {
|
||||
status = drain_helper(web_surface_process, process_generation,
|
||||
logical_generation);
|
||||
}
|
||||
if (status.ok()) {
|
||||
status = CloseUnclaimedSolvedChallenge();
|
||||
}
|
||||
if (status.ok() && browser_service_bridge != nullptr) {
|
||||
status = browser_service_bridge->DrainOutgoingEvents();
|
||||
}
|
||||
@@ -1096,10 +1236,10 @@ Status RobloxExperienceComposition::RefreshLateSurface() {
|
||||
SurfaceUpdateStatus(SurfaceCreated(event.surface.generation));
|
||||
break;
|
||||
case window::WindowSurfaceEventType::kChanged:
|
||||
status = SurfaceUpdateStatus(SurfaceChanged(
|
||||
{event.surface.generation, event.surface.native_window,
|
||||
event.surface.width, event.surface.height,
|
||||
event.surface.dpi_scale}));
|
||||
status = SurfaceUpdateStatus(
|
||||
SurfaceChanged({event.surface.generation,
|
||||
event.surface.native_window, event.surface.width,
|
||||
event.surface.height, event.surface.dpi_scale}));
|
||||
break;
|
||||
case window::WindowSurfaceEventType::kDestroyed:
|
||||
status =
|
||||
@@ -1223,14 +1363,16 @@ Status RobloxExperienceComposition::DrainExternalLaunchRequests() {
|
||||
|
||||
void RobloxExperienceComposition::NotifyLuaAppDidReturn() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (subscribed_) lua_app_return_pending_ = true;
|
||||
if (subscribed_)
|
||||
lua_app_return_pending_ = true;
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
Status completed_result = Status::Ok();
|
||||
if (launch_worker_.joinable()) {
|
||||
const OwnedPthreadWaitResult wait = launch_worker_.WaitFor(0, 1);
|
||||
if (wait.status == OwnedPthreadWaitStatus::kTimedOut) return Status::Ok();
|
||||
if (wait.status == OwnedPthreadWaitStatus::kTimedOut)
|
||||
return Status::Ok();
|
||||
if (!wait.joined()) {
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"could not reap experience launch worker: " +
|
||||
@@ -1290,7 +1432,8 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
}
|
||||
|
||||
Status external_launch_status = DrainExternalLaunchRequests();
|
||||
if (!external_launch_status.ok()) return external_launch_status;
|
||||
if (!external_launch_status.ok())
|
||||
return external_launch_status;
|
||||
|
||||
bool switch_active_game = false;
|
||||
std::optional<RobloxExperienceLaunchRequest> joining_request;
|
||||
@@ -1299,7 +1442,8 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
if (!subscribed_ || controller_ == nullptr || objects_ == nullptr) {
|
||||
return FailedPrecondition("experience composition is not subscribed");
|
||||
}
|
||||
if (controlled_switch_waiting_for_return_) return Status::Ok();
|
||||
if (controlled_switch_waiting_for_return_)
|
||||
return Status::Ok();
|
||||
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
||||
!pending_launch_requests_.empty()) {
|
||||
// Start the queued launch only after consuming this leave callback, so
|
||||
@@ -1324,9 +1468,12 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
if (!subscribed_ || controller_ == nullptr || objects_ == nullptr) {
|
||||
return FailedPrecondition("experience composition is not subscribed");
|
||||
}
|
||||
if (launch_in_progress_ || active_launch_ != nullptr) return Status::Ok();
|
||||
if (game_active_) return Status::Ok();
|
||||
if (pending_launch_requests_.empty()) return Status::Ok();
|
||||
if (launch_in_progress_ || active_launch_ != nullptr)
|
||||
return Status::Ok();
|
||||
if (game_active_)
|
||||
return Status::Ok();
|
||||
if (pending_launch_requests_.empty())
|
||||
return Status::Ok();
|
||||
if (next_request_id_ == 0 ||
|
||||
next_request_id_ == std::numeric_limits<uint64_t>::max()) {
|
||||
pending_launch_requests_.clear();
|
||||
@@ -1362,7 +1509,8 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
const int start_error =
|
||||
launch_worker_.Start(&RobloxExperienceComposition::RunLaunchWorker, this,
|
||||
kLaunchWorkerStackSize);
|
||||
if (start_error == 0) return Status::Ok();
|
||||
if (start_error == 0)
|
||||
return Status::Ok();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
launch_in_progress_ = false;
|
||||
@@ -1419,7 +1567,8 @@ void RobloxExperienceComposition::RunActiveLaunch() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
task = active_launch_.get();
|
||||
}
|
||||
if (task == nullptr) return;
|
||||
if (task == nullptr)
|
||||
return;
|
||||
Status status =
|
||||
BuildLaunchObjects(task->context.game_surface, &task->context.surface,
|
||||
&task->context.platform_params);
|
||||
@@ -1543,6 +1692,8 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
std::unique_ptr<RobloxWebViewBridge> web_view_bridge;
|
||||
std::unique_ptr<RobloxBrowserServiceBridge> browser_service_bridge;
|
||||
std::unique_ptr<RobloxPermissionsBridge> permissions_bridge;
|
||||
std::unique_ptr<RobloxCallProtocolBridge> call_protocol_bridge;
|
||||
std::unique_ptr<RobloxAccountBridge> account_bridge;
|
||||
std::shared_ptr<WebViewHelperProcess> web_surface_process;
|
||||
jnivm::VM* late_lifecycle_vm = nullptr;
|
||||
{
|
||||
@@ -1567,6 +1718,8 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
web_view_bridge = std::move(web_view_bridge_);
|
||||
browser_service_bridge = std::move(browser_service_bridge_);
|
||||
permissions_bridge = std::move(permissions_bridge_);
|
||||
call_protocol_bridge = std::move(call_protocol_bridge_);
|
||||
account_bridge = std::move(account_bridge_);
|
||||
web_surface_process = std::move(web_surface_process_);
|
||||
web_surface_logical_exit_observer_ = {};
|
||||
web_surface_route_ = WebSurfaceRoute::kNone;
|
||||
@@ -1583,12 +1736,23 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
if (web_surface_process != nullptr) {
|
||||
(void)web_surface_process->RequestClose();
|
||||
}
|
||||
Status status = permissions_bridge != nullptr ? permissions_bridge->Shutdown()
|
||||
: Status::Ok();
|
||||
Status status = call_protocol_bridge != nullptr
|
||||
? call_protocol_bridge->Shutdown()
|
||||
: Status::Ok();
|
||||
const Status account_status =
|
||||
account_bridge != nullptr ? account_bridge->Shutdown() : Status::Ok();
|
||||
if (status.ok())
|
||||
status = account_status;
|
||||
const Status permissions_status = permissions_bridge != nullptr
|
||||
? permissions_bridge->Shutdown()
|
||||
: Status::Ok();
|
||||
if (status.ok())
|
||||
status = permissions_status;
|
||||
const Status browser_status = browser_service_bridge != nullptr
|
||||
? browser_service_bridge->Shutdown()
|
||||
: Status::Ok();
|
||||
if (status.ok()) status = browser_status;
|
||||
if (status.ok())
|
||||
status = browser_status;
|
||||
const Status web_view_status =
|
||||
web_view_bridge != nullptr ? web_view_bridge->Shutdown() : Status::Ok();
|
||||
if (status.ok()) {
|
||||
@@ -1600,6 +1764,8 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
status = bridge_status;
|
||||
}
|
||||
browser_service_bridge.reset();
|
||||
call_protocol_bridge.reset();
|
||||
account_bridge.reset();
|
||||
permissions_bridge.reset();
|
||||
web_view_bridge.reset();
|
||||
bridge.reset();
|
||||
@@ -1617,14 +1783,16 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
launch_in_progress_ = false;
|
||||
lua_app_return_pending_ = false;
|
||||
if (active_launch_ != nullptr) {
|
||||
if (status.ok()) status = active_launch_->result;
|
||||
if (status.ok())
|
||||
status = active_launch_->result;
|
||||
active_launch_.reset();
|
||||
}
|
||||
controller = std::move(controller_);
|
||||
}
|
||||
if (controller != nullptr) {
|
||||
const Status controller_status = controller->Shutdown();
|
||||
if (status.ok()) status = controller_status;
|
||||
if (status.ok())
|
||||
status = controller_status;
|
||||
}
|
||||
const Status release = ReleaseGlobalObjects();
|
||||
return status.ok() ? release : status;
|
||||
@@ -1655,7 +1823,8 @@ void RobloxExperienceComposition::NotifyPresence(
|
||||
Status RobloxExperienceComposition::BuildPlatformGlobalObjects() {
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
jobject bus_local = nullptr;
|
||||
jclass bus_class =
|
||||
@@ -1669,7 +1838,8 @@ Status RobloxExperienceComposition::BuildPlatformGlobalObjects() {
|
||||
bus_local = singleton != nullptr
|
||||
? env->CallStaticObjectMethod(bus_class, singleton)
|
||||
: nullptr;
|
||||
if (bus_class != nullptr) env->DeleteLocalRef(bus_class);
|
||||
if (bus_class != nullptr)
|
||||
env->DeleteLocalRef(bus_class);
|
||||
status = bus_local != nullptr
|
||||
? CheckJni(env, "obtain exact MessageBus.f singleton")
|
||||
: Unavailable("MessageBus.f singleton is unavailable");
|
||||
@@ -1682,7 +1852,8 @@ Status RobloxExperienceComposition::BuildPlatformGlobalObjects() {
|
||||
}
|
||||
}
|
||||
|
||||
if (bus_local != nullptr) env->DeleteLocalRef(bus_local);
|
||||
if (bus_local != nullptr)
|
||||
env->DeleteLocalRef(bus_local);
|
||||
if (!status.ok()) {
|
||||
if (global->message_bus != nullptr)
|
||||
env->DeleteGlobalRef(global->message_bus);
|
||||
@@ -1700,7 +1871,8 @@ Status RobloxExperienceComposition::BuildPlatformGlobalObjects() {
|
||||
Status RobloxExperienceComposition::BuildLuaAppGlobalObjects() {
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
jclass native_gl_local =
|
||||
env->FindClass("com/roblox/engine/jni/NativeGLInterface");
|
||||
@@ -1724,12 +1896,17 @@ Status RobloxExperienceComposition::BuildLuaAppGlobalObjects() {
|
||||
}
|
||||
}
|
||||
|
||||
if (activity_local != nullptr) env->DeleteLocalRef(activity_local);
|
||||
if (activity_class != nullptr) env->DeleteLocalRef(activity_class);
|
||||
if (native_gl_local != nullptr) env->DeleteLocalRef(native_gl_local);
|
||||
if (activity_local != nullptr)
|
||||
env->DeleteLocalRef(activity_local);
|
||||
if (activity_class != nullptr)
|
||||
env->DeleteLocalRef(activity_class);
|
||||
if (native_gl_local != nullptr)
|
||||
env->DeleteLocalRef(native_gl_local);
|
||||
if (!status.ok()) {
|
||||
if (activity_global != nullptr) env->DeleteGlobalRef(activity_global);
|
||||
if (native_gl_global != nullptr) env->DeleteGlobalRef(native_gl_global);
|
||||
if (activity_global != nullptr)
|
||||
env->DeleteGlobalRef(activity_global);
|
||||
if (native_gl_global != nullptr)
|
||||
env->DeleteGlobalRef(native_gl_global);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -1756,7 +1933,8 @@ Status RobloxExperienceComposition::BuildLaunchObjects(
|
||||
*platform_params = nullptr;
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
jclass surface_class = nullptr;
|
||||
jobject surface_local = nullptr;
|
||||
@@ -1790,8 +1968,9 @@ Status RobloxExperienceComposition::BuildLaunchObjects(
|
||||
SetField<jobject>(env, params_local, params_class, "assetFolderPath",
|
||||
"Ljava/lang/String;", asset_path) &&
|
||||
SetField<jfloat>(env, params_local, params_class, "dpiScale", "F",
|
||||
surface.dpi_scale > 0.0f ? surface.dpi_scale
|
||||
: surface_config_.dpi_scale) &&
|
||||
surface.dpi_scale > 0.0f
|
||||
? surface.dpi_scale
|
||||
: surface_config_.dpi_scale) &&
|
||||
SetField<jint>(env, params_local, params_class, "viewportWidthMm", "I",
|
||||
surface_config_.viewport_width_mm) &&
|
||||
SetField<jint>(env, params_local, params_class, "viewportHeightMm", "I",
|
||||
@@ -1810,12 +1989,17 @@ Status RobloxExperienceComposition::BuildLaunchObjects(
|
||||
? CheckJni(env, "populate fresh launch PlatformParams")
|
||||
: Unavailable("required PlatformParams fields are unavailable");
|
||||
}
|
||||
if (asset_path != nullptr) env->DeleteLocalRef(asset_path);
|
||||
if (surface_class != nullptr) env->DeleteLocalRef(surface_class);
|
||||
if (params_class != nullptr) env->DeleteLocalRef(params_class);
|
||||
if (asset_path != nullptr)
|
||||
env->DeleteLocalRef(asset_path);
|
||||
if (surface_class != nullptr)
|
||||
env->DeleteLocalRef(surface_class);
|
||||
if (params_class != nullptr)
|
||||
env->DeleteLocalRef(params_class);
|
||||
if (!status.ok()) {
|
||||
if (params_local != nullptr) env->DeleteLocalRef(params_local);
|
||||
if (surface_local != nullptr) env->DeleteLocalRef(surface_local);
|
||||
if (params_local != nullptr)
|
||||
env->DeleteLocalRef(params_local);
|
||||
if (surface_local != nullptr)
|
||||
env->DeleteLocalRef(surface_local);
|
||||
return status;
|
||||
}
|
||||
*surface_object = surface_local;
|
||||
@@ -1886,13 +2070,19 @@ Status RobloxExperienceComposition::BuildLuaAppStartParams(
|
||||
"required LuaApp StartAppParams fields are unavailable");
|
||||
}
|
||||
|
||||
if (selected_theme != nullptr) env->DeleteLocalRef(selected_theme);
|
||||
if (username != nullptr) env->DeleteLocalRef(username);
|
||||
if (app_starter_script != nullptr) env->DeleteLocalRef(app_starter_script);
|
||||
if (app_starter_place != nullptr) env->DeleteLocalRef(app_starter_place);
|
||||
if (params_class != nullptr) env->DeleteLocalRef(params_class);
|
||||
if (selected_theme != nullptr)
|
||||
env->DeleteLocalRef(selected_theme);
|
||||
if (username != nullptr)
|
||||
env->DeleteLocalRef(username);
|
||||
if (app_starter_script != nullptr)
|
||||
env->DeleteLocalRef(app_starter_script);
|
||||
if (app_starter_place != nullptr)
|
||||
env->DeleteLocalRef(app_starter_place);
|
||||
if (params_class != nullptr)
|
||||
env->DeleteLocalRef(params_class);
|
||||
if (!status.ok()) {
|
||||
if (params_local != nullptr) env->DeleteLocalRef(params_local);
|
||||
if (params_local != nullptr)
|
||||
env->DeleteLocalRef(params_local);
|
||||
return status;
|
||||
}
|
||||
*start_app_params = params_local;
|
||||
@@ -1924,7 +2114,8 @@ Status RobloxExperienceComposition::RestartLuaAppSurface(
|
||||
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
jobject surface_object = nullptr;
|
||||
jobject platform_params = nullptr;
|
||||
@@ -1964,11 +2155,16 @@ Status RobloxExperienceComposition::RestartLuaAppSurface(
|
||||
status = CheckJni(env, "restart LuaApp with recreated surface");
|
||||
}
|
||||
|
||||
if (start_reason != nullptr) env->DeleteLocalRef(start_reason);
|
||||
if (stop_reason != nullptr) env->DeleteLocalRef(stop_reason);
|
||||
if (start_app_params != nullptr) env->DeleteLocalRef(start_app_params);
|
||||
if (platform_params != nullptr) env->DeleteLocalRef(platform_params);
|
||||
if (surface_object != nullptr) env->DeleteLocalRef(surface_object);
|
||||
if (start_reason != nullptr)
|
||||
env->DeleteLocalRef(start_reason);
|
||||
if (stop_reason != nullptr)
|
||||
env->DeleteLocalRef(stop_reason);
|
||||
if (start_app_params != nullptr)
|
||||
env->DeleteLocalRef(start_app_params);
|
||||
if (platform_params != nullptr)
|
||||
env->DeleteLocalRef(platform_params);
|
||||
if (surface_object != nullptr)
|
||||
env->DeleteLocalRef(surface_object);
|
||||
if (status.ok()) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
@@ -2028,7 +2224,8 @@ Status RobloxExperienceComposition::RebindLuaAppSurface(
|
||||
Status RobloxExperienceComposition::ReleaseGlobalObjects() {
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
std::unique_ptr<GlobalObjects> objects;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
@@ -2043,10 +2240,12 @@ Status RobloxExperienceComposition::ReleaseGlobalObjects() {
|
||||
pending_launch_requests_.clear();
|
||||
next_request_id_ = 1;
|
||||
}
|
||||
if (objects == nullptr) return Status::Ok();
|
||||
if (objects == nullptr)
|
||||
return Status::Ok();
|
||||
if (objects->message_bus != nullptr)
|
||||
env->DeleteGlobalRef(objects->message_bus);
|
||||
if (objects->activity != nullptr) env->DeleteGlobalRef(objects->activity);
|
||||
if (objects->activity != nullptr)
|
||||
env->DeleteGlobalRef(objects->activity);
|
||||
if (objects->native_gl_class != nullptr)
|
||||
env->DeleteGlobalRef(objects->native_gl_class);
|
||||
return CheckJni(env, "release experience composition JNI roots");
|
||||
@@ -2055,7 +2254,8 @@ Status RobloxExperienceComposition::ReleaseGlobalObjects() {
|
||||
Status RobloxExperienceComposition::ReleaseLuaAppGlobalObjects() {
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
jclass native_gl_class = nullptr;
|
||||
jobject activity = nullptr;
|
||||
@@ -2075,8 +2275,10 @@ Status RobloxExperienceComposition::ReleaseLuaAppGlobalObjects() {
|
||||
pending_launch_requests_.clear();
|
||||
next_request_id_ = 1;
|
||||
}
|
||||
if (activity != nullptr) env->DeleteGlobalRef(activity);
|
||||
if (native_gl_class != nullptr) env->DeleteGlobalRef(native_gl_class);
|
||||
if (activity != nullptr)
|
||||
env->DeleteGlobalRef(activity);
|
||||
if (native_gl_class != nullptr)
|
||||
env->DeleteGlobalRef(native_gl_class);
|
||||
return CheckJni(env, "release LuaApp experience JNI roots");
|
||||
}
|
||||
|
||||
|
||||
@@ -274,14 +274,15 @@ AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode,
|
||||
return {};
|
||||
}
|
||||
|
||||
int32_t MapSdlMouseButtonToAndroid(uint8_t sdl_button) {
|
||||
int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button) {
|
||||
switch (sdl_button) {
|
||||
case SDL_BUTTON_LEFT:
|
||||
return 0;
|
||||
case SDL_BUTTON_RIGHT:
|
||||
return 1;
|
||||
case SDL_BUTTON_MIDDLE:
|
||||
return 3;
|
||||
// MouseButton3 is index 2, not BUTTON_TERTIARY (4) minus one.
|
||||
return 2;
|
||||
case SDL_BUTTON_X1:
|
||||
return 7;
|
||||
case SDL_BUTTON_X2:
|
||||
@@ -586,11 +587,11 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked(
|
||||
RobloxInputEventKind::kMouseButton,
|
||||
Unsupported("native mouse buttons are unavailable"));
|
||||
}
|
||||
const int32_t android_button = MapSdlMouseButtonToAndroid(event.button);
|
||||
if (android_button < 0) {
|
||||
const int32_t roblox_button = MapSdlMouseButtonToRoblox(event.button);
|
||||
if (roblox_button < 0) {
|
||||
return Result(RobloxInputDispatchState::kIgnoredUnsupported,
|
||||
RobloxInputEventKind::kMouseButton,
|
||||
Unsupported("SDL mouse button has no Android mapping"));
|
||||
Unsupported("SDL mouse button has no Roblox mapping"));
|
||||
}
|
||||
const platform::SurfaceCoordinateTransform transform =
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
@@ -608,20 +609,14 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked(
|
||||
}
|
||||
const float clamped_x = mouse_x_;
|
||||
const float clamped_y = mouse_y_;
|
||||
if (event.pressed) {
|
||||
const RobloxTextEditorSnapshot text = text_editor_.Snapshot();
|
||||
if (text.focused) {
|
||||
(void)text_editor_.EndFocusSession(text.textbox_handle, text.generation,
|
||||
true);
|
||||
}
|
||||
}
|
||||
// Let Roblox decide whether a click releases TextBox focus.
|
||||
Status status = sink_.mouse_button(sink_.context, clamped_x, clamped_y,
|
||||
event.pressed, android_button);
|
||||
event.pressed, roblox_button);
|
||||
if (status.ok()) {
|
||||
const auto active = std::find(active_mouse_buttons_.begin(),
|
||||
active_mouse_buttons_.end(), android_button);
|
||||
active_mouse_buttons_.end(), roblox_button);
|
||||
if (event.pressed && active == active_mouse_buttons_.end()) {
|
||||
active_mouse_buttons_.push_back(android_button);
|
||||
active_mouse_buttons_.push_back(roblox_button);
|
||||
} else if (!event.pressed && active != active_mouse_buttons_.end()) {
|
||||
active_mouse_buttons_.erase(active);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
// [patch.py] RequestFollowUser support
|
||||
#include "runtime/roblox_presence_resolver.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
@@ -541,14 +544,42 @@ Status ValidateSelector(const LaunchFields& fields) {
|
||||
? Status::Ok()
|
||||
: Invalid("RequestPlayTogetherGame requires a conversation");
|
||||
}
|
||||
// [patch.py] RequestFollowUser support
|
||||
if (request == "requestfollowuser") {
|
||||
// By the time ValidateSelector runs, MakeRequest has already
|
||||
// resolved user_id -> place_id and cleared user_id, so check the
|
||||
// resolved place_id here rather than the now-zeroed user_id.
|
||||
return fields.place_id > 0
|
||||
? Status::Ok()
|
||||
: Invalid("RequestFollowUser could not resolve a place");
|
||||
}
|
||||
return Invalid("Roblox PlaceLauncher request type is unsupported");
|
||||
}
|
||||
|
||||
Status MakeRequest(const LaunchFields& fields,
|
||||
// [patch.py] RequestFollowUser support
|
||||
Status MakeRequest(const LaunchFields& fields_in,
|
||||
RobloxExperienceLaunchRequest* request) {
|
||||
if (fields.place_id <= 0) {
|
||||
LaunchFields fields = fields_in;
|
||||
const bool is_follow_user =
|
||||
AsciiLower(fields.request_type) == "requestfollowuser";
|
||||
if (fields.place_id <= 0 && !is_follow_user) {
|
||||
return Invalid("Roblox launch URI requires a positive placeId");
|
||||
}
|
||||
if (fields.place_id <= 0 && is_follow_user) {
|
||||
ResolvedFollowUserPlace resolved;
|
||||
const Status resolve_status = ResolveFollowUserPlace(
|
||||
fields.user_id, ReadStoredRoblosecurityCookie(), &resolved);
|
||||
if (!resolve_status.ok()) {
|
||||
return resolve_status;
|
||||
}
|
||||
fields.place_id = resolved.place_id;
|
||||
if (!resolved.game_instance_id.empty()) {
|
||||
fields.game_instance_id = resolved.game_instance_id;
|
||||
}
|
||||
// user_id was only needed to resolve which place to join; clear
|
||||
// it so ValidateSelector does not see two conflicting selectors.
|
||||
fields.user_id = 0;
|
||||
}
|
||||
Status status = ValidateSelector(fields);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
@@ -640,5 +671,64 @@ Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
return MakeRequest(fields, request);
|
||||
}
|
||||
|
||||
Status ParseRobloxWebViewLaunchGame(std::string_view command,
|
||||
RobloxExperienceLaunchRequest* request) {
|
||||
if (request == nullptr) {
|
||||
return Invalid("Roblox launchGame output is null");
|
||||
}
|
||||
*request = {};
|
||||
const nlohmann::json document =
|
||||
command.size() <= kMaximumRobloxLaunchUriBytes
|
||||
? nlohmann::json::parse(command, nullptr, false)
|
||||
: nlohmann::json();
|
||||
const auto function_name = document.is_object()
|
||||
? document.find("functionName")
|
||||
: document.end();
|
||||
if (!document.is_object() || function_name == document.end() ||
|
||||
*function_name != "launchGame") {
|
||||
return Status::Error(StatusCode::kUnsupported,
|
||||
"WebView command is not launchGame");
|
||||
}
|
||||
const auto params = document.find("params");
|
||||
if (params == document.end() || !params->is_object()) {
|
||||
return Invalid("Roblox launchGame has no params");
|
||||
}
|
||||
const auto launch = params->find("request");
|
||||
if (launch == params->end() || !launch->is_object()) {
|
||||
return Invalid("Roblox launchGame has no request");
|
||||
}
|
||||
if (launch->size() > kMaximumParameterCount) {
|
||||
return Invalid("Roblox launchGame request has too many fields");
|
||||
}
|
||||
LaunchFields fields;
|
||||
for (const auto& [key, value] : launch->items()) {
|
||||
std::string text;
|
||||
if (value.is_string()) {
|
||||
text = value.get<std::string>();
|
||||
} else if (value.is_number_unsigned()) {
|
||||
text = std::to_string(value.get<uint64_t>());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (text.size() > kMaximumParameterValueBytes ||
|
||||
ContainsControlBytes(text)) {
|
||||
return Invalid("Roblox launchGame field is invalid");
|
||||
}
|
||||
Status status = AssignField(key == "requestType" ? "request" : key,
|
||||
std::move(text), false, &fields);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
if (fields.request_type.empty()) {
|
||||
return Invalid("Roblox launchGame request type is missing");
|
||||
}
|
||||
Status status = MakeRequest(fields, request);
|
||||
if (!status.ok()) {
|
||||
*request = {};
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -0,0 +1,295 @@
|
||||
#include "runtime/roblox_message_bus_request_bridge.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
Status Unavailable(const char* message) {
|
||||
return Status::Error(StatusCode::kUnavailable, message);
|
||||
}
|
||||
|
||||
Status CheckJni(JNIEnv* env) {
|
||||
if (!env->ExceptionCheck())
|
||||
return Status::Ok();
|
||||
env->ExceptionClear();
|
||||
return Unavailable("MessageBus JNI operation failed");
|
||||
}
|
||||
|
||||
// JNI local references are never retained across callback invocations.
|
||||
struct LocalString {
|
||||
JNIEnv* env;
|
||||
jstring value;
|
||||
|
||||
LocalString(JNIEnv* source, const char* text)
|
||||
: env(source), value(source->NewStringUTF(text)) {}
|
||||
|
||||
~LocalString() {
|
||||
if (value)
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct RobloxMessageBusRequestBridge::State {
|
||||
struct Endpoint {
|
||||
std::shared_ptr<State> state;
|
||||
std::size_t method;
|
||||
};
|
||||
|
||||
struct Registration {
|
||||
jobject callback = nullptr;
|
||||
jobject handler = nullptr;
|
||||
jobject connection = nullptr;
|
||||
jlong connection_handle = 0;
|
||||
jstring method = nullptr;
|
||||
bool handler_registered = false;
|
||||
};
|
||||
|
||||
RobloxMessageBusSymbols symbols;
|
||||
jobject bus = nullptr;
|
||||
jstring protocol = nullptr;
|
||||
std::string protocol_name;
|
||||
std::vector<std::string> methods;
|
||||
Handler handler;
|
||||
std::vector<Registration> registrations;
|
||||
std::mutex mutex;
|
||||
std::condition_variable drained;
|
||||
bool accepting = true;
|
||||
std::size_t in_flight = 0;
|
||||
|
||||
static void Legacy(void* context, JNIEnv* env, jstring message) {
|
||||
Dispatch(context, env, message, nullptr);
|
||||
}
|
||||
|
||||
static void Async(void* context, JNIEnv* env, jstring message, jstring id) {
|
||||
Dispatch(context, env, message, id);
|
||||
}
|
||||
|
||||
static void Dispatch(void* context, JNIEnv* env, jstring message,
|
||||
jstring id) {
|
||||
const auto* endpoint = static_cast<Endpoint*>(context);
|
||||
const auto state = endpoint->state;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (!state->accepting)
|
||||
return;
|
||||
++state->in_flight;
|
||||
}
|
||||
|
||||
struct Finish {
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
~Finish() {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
--state->in_flight;
|
||||
state->drained.notify_all();
|
||||
}
|
||||
} finish{state};
|
||||
|
||||
const auto response = state->handler(env, message, endpoint->method);
|
||||
// Request bodies and correlation IDs must never be logged.
|
||||
std::fprintf(stderr, " [messagebus] %s method=%s result=%s transport=%s\n",
|
||||
state->protocol_name.c_str(),
|
||||
state->methods[endpoint->method].c_str(), response.outcome,
|
||||
id ? "async" : "legacy");
|
||||
LocalString body(env, response.json.c_str());
|
||||
LocalString telemetry(env, "{}");
|
||||
LocalString method(env, state->methods[endpoint->method].c_str());
|
||||
const Status allocated = CheckJni(env);
|
||||
if (!body.value || !telemetry.value || !method.value || !allocated.ok()) {
|
||||
std::fprintf(stderr, " [messagebus] response allocation failed\n");
|
||||
return;
|
||||
}
|
||||
// The APK publishes the legacy response even for the newer async API,
|
||||
// then resolves the individual request by its opaque response ID.
|
||||
state->symbols.publish_response(env, state->bus, state->protocol,
|
||||
method.value, body.value, response.code,
|
||||
telemetry.value);
|
||||
const Status published = CheckJni(env);
|
||||
if (id) {
|
||||
state->symbols.call_response_handler(env, state->bus, id, body.value);
|
||||
}
|
||||
if (!CheckJni(env).ok() || !published.ok()) {
|
||||
std::fprintf(stderr, " [messagebus] response delivery failed\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RobloxMessageBusRequestBridge::RobloxMessageBusRequestBridge(
|
||||
JniEnvironmentProvider environment, RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects, std::string protocol,
|
||||
std::vector<std::string> methods, Handler handler)
|
||||
: environment_(environment),
|
||||
symbols_(symbols),
|
||||
objects_(objects),
|
||||
protocol_name_(std::move(protocol)),
|
||||
methods_(std::move(methods)),
|
||||
handler_(std::move(handler)) {}
|
||||
|
||||
RobloxMessageBusRequestBridge::~RobloxMessageBusRequestBridge() {
|
||||
(void)Shutdown();
|
||||
}
|
||||
|
||||
Status RobloxMessageBusRequestBridge::Initialize() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
if (state_) {
|
||||
return Status::Error(StatusCode::kFailedPrecondition,
|
||||
"MessageBus request bridge is already initialized");
|
||||
}
|
||||
if (!environment_.valid() || !symbols_.complete() || !objects_.complete() ||
|
||||
protocol_name_.empty() || methods_.empty() || methods_.size() > 64 ||
|
||||
!handler_) {
|
||||
return Unavailable(
|
||||
"MessageBus request bridge prerequisites are incomplete");
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
state_ = std::make_shared<State>();
|
||||
state_->symbols = symbols_;
|
||||
state_->protocol_name = protocol_name_;
|
||||
state_->methods = methods_;
|
||||
state_->handler = handler_;
|
||||
state_->registrations.resize(methods_.size());
|
||||
state_->bus = env->NewGlobalRef(objects_.message_bus);
|
||||
LocalString protocol(env, protocol_name_.c_str());
|
||||
state_->protocol = static_cast<jstring>(env->NewGlobalRef(protocol.value));
|
||||
auto fail = [this](Status failure) {
|
||||
(void)ShutdownLocked();
|
||||
return failure;
|
||||
};
|
||||
if (!state_->bus || !state_->protocol || !CheckJni(env).ok()) {
|
||||
return fail(
|
||||
Unavailable("could not retain MessageBus request bridge roots"));
|
||||
}
|
||||
for (std::size_t i = 0; i < methods_.size(); ++i) {
|
||||
auto& entry = state_->registrations[i];
|
||||
auto endpoint =
|
||||
std::make_shared<State::Endpoint>(State::Endpoint{state_, i});
|
||||
LocalString method(env, methods_[i].c_str());
|
||||
entry.method = static_cast<jstring>(env->NewGlobalRef(method.value));
|
||||
jobject callback = objects_.create_raw_callback(objects_.context, endpoint,
|
||||
&State::Legacy);
|
||||
if (callback) {
|
||||
entry.callback = env->NewGlobalRef(callback);
|
||||
if (!entry.callback)
|
||||
objects_.clear_raw_callback(objects_.context, callback);
|
||||
env->DeleteLocalRef(callback);
|
||||
}
|
||||
jobject handler = objects_.create_async_handler(objects_.context, endpoint,
|
||||
&State::Async);
|
||||
if (handler) {
|
||||
entry.handler = env->NewGlobalRef(handler);
|
||||
if (!entry.handler)
|
||||
objects_.clear_async_handler(objects_.context, handler);
|
||||
env->DeleteLocalRef(handler);
|
||||
}
|
||||
if (!entry.method || !entry.callback || !entry.handler ||
|
||||
!CheckJni(env).ok()) {
|
||||
return fail(
|
||||
Unavailable("could not create MessageBus request bridge callbacks"));
|
||||
}
|
||||
jobject connection =
|
||||
symbols_.subscribe_request(env, state_->bus, state_->protocol,
|
||||
entry.method, entry.callback, JNI_FALSE);
|
||||
if (connection) {
|
||||
jclass cls = env->GetObjectClass(connection);
|
||||
jfieldID pointer = cls ? env->GetFieldID(cls, "a", "J") : nullptr;
|
||||
if (pointer)
|
||||
entry.connection_handle = env->GetLongField(connection, pointer);
|
||||
if (cls)
|
||||
env->DeleteLocalRef(cls);
|
||||
entry.connection = env->NewGlobalRef(connection);
|
||||
if (!entry.connection && entry.connection_handle) {
|
||||
symbols_.delete_connection(env, connection, entry.connection_handle);
|
||||
entry.connection_handle = 0;
|
||||
}
|
||||
env->DeleteLocalRef(connection);
|
||||
}
|
||||
if (!entry.connection || !entry.connection_handle || !CheckJni(env).ok()) {
|
||||
return fail(
|
||||
Unavailable("could not subscribe MessageBus request bridge request"));
|
||||
}
|
||||
entry.handler_registered = true;
|
||||
symbols_.set_async_handler(env, state_->bus, state_->protocol, entry.method,
|
||||
entry.handler);
|
||||
status = CheckJni(env);
|
||||
if (!status.ok())
|
||||
return fail(status);
|
||||
}
|
||||
std::fprintf(stderr, " [messagebus] %s ready legacy=%zu async=%zu\n",
|
||||
protocol_name_.c_str(), methods_.size(), methods_.size());
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Status RobloxMessageBusRequestBridge::Shutdown() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
return ShutdownLocked();
|
||||
}
|
||||
|
||||
Status RobloxMessageBusRequestBridge::ShutdownLocked() {
|
||||
if (!state_)
|
||||
return Status::Ok();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_->mutex);
|
||||
state_->accepting = false;
|
||||
state_->drained.wait(lock, [this] {
|
||||
return state_->in_flight == 0;
|
||||
});
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok())
|
||||
return status; // Keep roots for a later shutdown retry.
|
||||
// Clear any exception from partially failed initialization before cleanup.
|
||||
const Status pending = CheckJni(env);
|
||||
if (!pending.ok())
|
||||
status = pending;
|
||||
for (auto& entry : state_->registrations) {
|
||||
if (entry.handler_registered) {
|
||||
symbols_.clear_handler(env, state_->bus, state_->protocol, entry.method);
|
||||
const Status cleared = CheckJni(env);
|
||||
if (!cleared.ok())
|
||||
status = cleared;
|
||||
entry.handler_registered = false;
|
||||
}
|
||||
if (entry.connection && entry.connection_handle) {
|
||||
symbols_.delete_connection(env, entry.connection,
|
||||
entry.connection_handle);
|
||||
const Status disconnected = CheckJni(env);
|
||||
if (!disconnected.ok())
|
||||
status = disconnected;
|
||||
entry.connection_handle = 0;
|
||||
}
|
||||
if (entry.callback) {
|
||||
objects_.clear_raw_callback(objects_.context, entry.callback);
|
||||
env->DeleteGlobalRef(entry.callback);
|
||||
}
|
||||
if (entry.handler) {
|
||||
objects_.clear_async_handler(objects_.context, entry.handler);
|
||||
env->DeleteGlobalRef(entry.handler);
|
||||
}
|
||||
if (entry.connection)
|
||||
env->DeleteGlobalRef(entry.connection);
|
||||
if (entry.method)
|
||||
env->DeleteGlobalRef(entry.method);
|
||||
entry = {};
|
||||
const Status cleared = CheckJni(env);
|
||||
if (!cleared.ok())
|
||||
status = cleared;
|
||||
}
|
||||
if (state_->protocol)
|
||||
env->DeleteGlobalRef(state_->protocol);
|
||||
if (state_->bus)
|
||||
env->DeleteGlobalRef(state_->bus);
|
||||
state_.reset();
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -1,46 +1,14 @@
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr char kProtocol[] = "PermissionsProtocol";
|
||||
constexpr char kMicrophone[] = "MICROPHONE_ACCESS";
|
||||
constexpr char kLocalNetwork[] = "LOCAL_NETWORK";
|
||||
constexpr const char* kMethods[] = {
|
||||
"SupportsPermissions", "HasPermissions", "PermissionsRequest",
|
||||
"ShouldShowPermissionUpsell", "ShouldShowRequestPermissionRationale"};
|
||||
constexpr std::size_t kMethodCount = std::size(kMethods);
|
||||
constexpr std::size_t kMaximumJsonBytes = 64 * 1024;
|
||||
constexpr std::size_t kMaximumFeatures = 64;
|
||||
|
||||
Status Unavailable(const char* message) {
|
||||
return Status::Error(StatusCode::kUnavailable, message);
|
||||
}
|
||||
|
||||
Status CheckJni(JNIEnv* env) {
|
||||
if (!env->ExceptionCheck()) return Status::Ok();
|
||||
env->ExceptionClear();
|
||||
return Unavailable("PermissionsProtocol JNI operation failed");
|
||||
}
|
||||
|
||||
// JNI local references are never retained across callback invocations.
|
||||
struct LocalString {
|
||||
JNIEnv* env;
|
||||
jstring value;
|
||||
LocalString(JNIEnv* source, const char* text)
|
||||
: env(source), value(source->NewStringUTF(text)) {}
|
||||
~LocalString() {
|
||||
if (value) env->DeleteLocalRef(value);
|
||||
}
|
||||
};
|
||||
constexpr char kMicrophone[] = "MICROPHONE_ACCESS";
|
||||
constexpr char kLocalNetwork[] = "LOCAL_NETWORK";
|
||||
|
||||
struct Response {
|
||||
nlohmann::json body;
|
||||
@@ -63,7 +31,8 @@ bool HasBoundedDepth(const std::string& json) {
|
||||
} else if (ch == '"') {
|
||||
quoted = true;
|
||||
} else if (ch == '{' || ch == '[') {
|
||||
if (++depth > 8) return false;
|
||||
if (++depth > 8)
|
||||
return false;
|
||||
} else if (ch == '}' || ch == ']') {
|
||||
--depth;
|
||||
}
|
||||
@@ -82,18 +51,22 @@ Response Answer(JNIEnv* env, jstring message, std::size_t method,
|
||||
: nlohmann::json{{"status", "DENIED"},
|
||||
{"missingPermissions", {kMicrophone}}},
|
||||
13, "invalid_request"};
|
||||
if (!message) return rejected;
|
||||
if (!message)
|
||||
return rejected;
|
||||
const jsize size = env->GetStringUTFLength(message);
|
||||
if (size <= 0 || static_cast<std::size_t>(size) > kMaximumJsonBytes) {
|
||||
return rejected;
|
||||
}
|
||||
const char* bytes = env->GetStringUTFChars(message, nullptr);
|
||||
if (!bytes) return rejected;
|
||||
if (!bytes)
|
||||
return rejected;
|
||||
const std::string owned(bytes, static_cast<std::size_t>(size));
|
||||
env->ReleaseStringUTFChars(message, bytes);
|
||||
if (!HasBoundedDepth(owned)) return rejected;
|
||||
if (!HasBoundedDepth(owned))
|
||||
return rejected;
|
||||
const auto request = nlohmann::json::parse(owned, nullptr, false);
|
||||
if (!request.is_object()) return rejected;
|
||||
if (!request.is_object())
|
||||
return rejected;
|
||||
if (support) {
|
||||
return {{{"permissions", {kMicrophone, kLocalNetwork}}}, 0, "supported"};
|
||||
}
|
||||
@@ -104,7 +77,8 @@ Response Answer(JNIEnv* env, jstring message, std::size_t method,
|
||||
}
|
||||
auto missing = nlohmann::json::array();
|
||||
for (const auto& feature : *features) {
|
||||
if (!feature.is_string()) return rejected;
|
||||
if (!feature.is_string())
|
||||
return rejected;
|
||||
const auto& name = feature.get_ref<const std::string&>();
|
||||
if (name.empty() || name.size() > 128 ||
|
||||
name.find_first_of("\r\n\t") != std::string::npos ||
|
||||
@@ -132,233 +106,31 @@ Response Answer(JNIEnv* env, jstring message, std::size_t method,
|
||||
|
||||
} // namespace
|
||||
|
||||
struct RobloxPermissionsBridge::State {
|
||||
struct Endpoint {
|
||||
std::shared_ptr<State> state;
|
||||
std::size_t method;
|
||||
};
|
||||
struct Registration {
|
||||
jobject callback = nullptr;
|
||||
jobject handler = nullptr;
|
||||
jobject connection = nullptr;
|
||||
jlong connection_handle = 0;
|
||||
jstring method = nullptr;
|
||||
bool handler_registered = false;
|
||||
};
|
||||
|
||||
RobloxPermissionsMessageBusSymbols symbols;
|
||||
jobject bus = nullptr;
|
||||
jstring protocol = nullptr;
|
||||
bool microphone_enabled = false;
|
||||
std::array<Registration, kMethodCount> registrations;
|
||||
std::mutex mutex;
|
||||
std::condition_variable drained;
|
||||
bool accepting = true;
|
||||
std::size_t in_flight = 0;
|
||||
|
||||
static void Legacy(void* context, JNIEnv* env, jstring message) {
|
||||
Dispatch(context, env, message, nullptr);
|
||||
}
|
||||
static void Async(void* context, JNIEnv* env, jstring message, jstring id) {
|
||||
Dispatch(context, env, message, id);
|
||||
}
|
||||
static void Dispatch(void* context, JNIEnv* env, jstring message,
|
||||
jstring id) {
|
||||
const auto* endpoint = static_cast<Endpoint*>(context);
|
||||
const auto state = endpoint->state;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (!state->accepting) return;
|
||||
++state->in_flight;
|
||||
}
|
||||
struct Finish {
|
||||
std::shared_ptr<State> state;
|
||||
~Finish() {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
--state->in_flight;
|
||||
state->drained.notify_all();
|
||||
}
|
||||
} finish{state};
|
||||
|
||||
Response response =
|
||||
Answer(env, message, endpoint->method, state->microphone_enabled);
|
||||
// No payload, feature strings, or correlation IDs in logs.
|
||||
std::fprintf(stderr,
|
||||
" [permissions] %s method=%s result=%s microphone=%s "
|
||||
"transport=%s\n",
|
||||
kProtocol, kMethods[endpoint->method], response.outcome,
|
||||
state->microphone_enabled ? "enabled" : "disabled",
|
||||
id ? "async" : "legacy");
|
||||
LocalString body(env, response.body.dump().c_str());
|
||||
LocalString telemetry(env, "{}");
|
||||
LocalString method(env, kMethods[endpoint->method]);
|
||||
const Status allocated = CheckJni(env);
|
||||
if (!body.value || !telemetry.value || !method.value || !allocated.ok()) {
|
||||
std::fprintf(stderr, " [permissions] response allocation failed\n");
|
||||
return;
|
||||
}
|
||||
// The APK publishes the legacy response even for the newer async API,
|
||||
// then resolves the individual request by its opaque response ID.
|
||||
state->symbols.publish_response(env, state->bus, state->protocol,
|
||||
method.value, body.value, response.code,
|
||||
telemetry.value);
|
||||
const Status published = CheckJni(env);
|
||||
if (id) {
|
||||
state->symbols.call_response_handler(env, state->bus, id, body.value);
|
||||
}
|
||||
if (!CheckJni(env).ok() || !published.ok()) {
|
||||
std::fprintf(stderr, " [permissions] response delivery failed\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RobloxPermissionsBridge::RobloxPermissionsBridge(
|
||||
JniEnvironmentProvider environment,
|
||||
RobloxPermissionsMessageBusSymbols symbols,
|
||||
RobloxPermissionsMessageBusObjects objects, bool microphone_enabled)
|
||||
: environment_(environment),
|
||||
symbols_(symbols),
|
||||
objects_(objects),
|
||||
microphone_enabled_(microphone_enabled) {}
|
||||
: bridge_(std::make_unique<RobloxMessageBusRequestBridge>(
|
||||
environment, symbols, objects, "PermissionsProtocol",
|
||||
std::vector<std::string>{"SupportsPermissions", "HasPermissions",
|
||||
"PermissionsRequest",
|
||||
"ShouldShowPermissionUpsell",
|
||||
"ShouldShowRequestPermissionRationale"},
|
||||
[microphone_enabled](JNIEnv* env, jstring message,
|
||||
std::size_t method) {
|
||||
auto response = Answer(env, message, method, microphone_enabled);
|
||||
return RobloxMessageBusReply{response.body.dump(), response.code,
|
||||
response.outcome};
|
||||
})) {}
|
||||
|
||||
RobloxPermissionsBridge::~RobloxPermissionsBridge() { (void)Shutdown(); }
|
||||
RobloxPermissionsBridge::~RobloxPermissionsBridge() = default;
|
||||
|
||||
Status RobloxPermissionsBridge::Initialize() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
if (state_) {
|
||||
return Status::Error(StatusCode::kFailedPrecondition,
|
||||
"PermissionsProtocol is already initialized");
|
||||
}
|
||||
if (!environment_.valid() || !symbols_.complete() || !objects_.complete()) {
|
||||
return Unavailable("PermissionsProtocol prerequisites are incomplete");
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status;
|
||||
state_ = std::make_shared<State>();
|
||||
state_->symbols = symbols_;
|
||||
state_->microphone_enabled = microphone_enabled_;
|
||||
state_->bus = env->NewGlobalRef(objects_.message_bus);
|
||||
LocalString protocol(env, kProtocol);
|
||||
state_->protocol = static_cast<jstring>(env->NewGlobalRef(protocol.value));
|
||||
auto fail = [this](Status failure) {
|
||||
(void)ShutdownLocked();
|
||||
return failure;
|
||||
};
|
||||
if (!state_->bus || !state_->protocol || !CheckJni(env).ok()) {
|
||||
return fail(Unavailable("could not retain PermissionsProtocol roots"));
|
||||
}
|
||||
for (std::size_t i = 0; i < kMethodCount; ++i) {
|
||||
auto& entry = state_->registrations[i];
|
||||
auto endpoint =
|
||||
std::make_shared<State::Endpoint>(State::Endpoint{state_, i});
|
||||
LocalString method(env, kMethods[i]);
|
||||
entry.method = static_cast<jstring>(env->NewGlobalRef(method.value));
|
||||
jobject callback = objects_.create_raw_callback(objects_.context, endpoint,
|
||||
&State::Legacy);
|
||||
if (callback) {
|
||||
entry.callback = env->NewGlobalRef(callback);
|
||||
if (!entry.callback)
|
||||
objects_.clear_raw_callback(objects_.context, callback);
|
||||
env->DeleteLocalRef(callback);
|
||||
}
|
||||
jobject handler = objects_.create_async_handler(objects_.context, endpoint,
|
||||
&State::Async);
|
||||
if (handler) {
|
||||
entry.handler = env->NewGlobalRef(handler);
|
||||
if (!entry.handler)
|
||||
objects_.clear_async_handler(objects_.context, handler);
|
||||
env->DeleteLocalRef(handler);
|
||||
}
|
||||
if (!entry.method || !entry.callback || !entry.handler ||
|
||||
!CheckJni(env).ok()) {
|
||||
return fail(
|
||||
Unavailable("could not create PermissionsProtocol callbacks"));
|
||||
}
|
||||
jobject connection =
|
||||
symbols_.subscribe_request(env, state_->bus, state_->protocol,
|
||||
entry.method, entry.callback, JNI_FALSE);
|
||||
if (connection) {
|
||||
jclass cls = env->GetObjectClass(connection);
|
||||
jfieldID pointer = cls ? env->GetFieldID(cls, "a", "J") : nullptr;
|
||||
if (pointer)
|
||||
entry.connection_handle = env->GetLongField(connection, pointer);
|
||||
if (cls) env->DeleteLocalRef(cls);
|
||||
entry.connection = env->NewGlobalRef(connection);
|
||||
if (!entry.connection && entry.connection_handle) {
|
||||
symbols_.delete_connection(env, connection, entry.connection_handle);
|
||||
entry.connection_handle = 0;
|
||||
}
|
||||
env->DeleteLocalRef(connection);
|
||||
}
|
||||
if (!entry.connection || !entry.connection_handle || !CheckJni(env).ok()) {
|
||||
return fail(
|
||||
Unavailable("could not subscribe PermissionsProtocol request"));
|
||||
}
|
||||
entry.handler_registered = true;
|
||||
symbols_.set_async_handler(env, state_->bus, state_->protocol, entry.method,
|
||||
entry.handler);
|
||||
status = CheckJni(env);
|
||||
if (!status.ok()) return fail(status);
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [permissions] PermissionsProtocol ready microphone=%s "
|
||||
"legacy=5 async=5\n",
|
||||
microphone_enabled_ ? "enabled" : "disabled");
|
||||
return Status::Ok();
|
||||
return bridge_->Initialize();
|
||||
}
|
||||
|
||||
Status RobloxPermissionsBridge::Shutdown() {
|
||||
std::lock_guard<std::mutex> lifecycle(lifecycle_mutex_);
|
||||
return ShutdownLocked();
|
||||
return bridge_->Shutdown();
|
||||
}
|
||||
|
||||
Status RobloxPermissionsBridge::ShutdownLocked() {
|
||||
if (!state_) return Status::Ok();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_->mutex);
|
||||
state_->accepting = false;
|
||||
state_->drained.wait(lock, [this] { return state_->in_flight == 0; });
|
||||
}
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
if (!status.ok()) return status; // Keep roots for a later shutdown retry.
|
||||
// Clear any exception from partially failed initialization before cleanup.
|
||||
const Status pending = CheckJni(env);
|
||||
if (!pending.ok()) status = pending;
|
||||
for (auto& entry : state_->registrations) {
|
||||
if (entry.handler_registered) {
|
||||
symbols_.clear_handler(env, state_->bus, state_->protocol, entry.method);
|
||||
const Status cleared = CheckJni(env);
|
||||
if (!cleared.ok()) status = cleared;
|
||||
entry.handler_registered = false;
|
||||
}
|
||||
if (entry.connection && entry.connection_handle) {
|
||||
symbols_.delete_connection(env, entry.connection,
|
||||
entry.connection_handle);
|
||||
const Status disconnected = CheckJni(env);
|
||||
if (!disconnected.ok()) status = disconnected;
|
||||
entry.connection_handle = 0;
|
||||
}
|
||||
if (entry.callback) {
|
||||
objects_.clear_raw_callback(objects_.context, entry.callback);
|
||||
env->DeleteGlobalRef(entry.callback);
|
||||
}
|
||||
if (entry.handler) {
|
||||
objects_.clear_async_handler(objects_.context, entry.handler);
|
||||
env->DeleteGlobalRef(entry.handler);
|
||||
}
|
||||
if (entry.connection) env->DeleteGlobalRef(entry.connection);
|
||||
if (entry.method) env->DeleteGlobalRef(entry.method);
|
||||
entry = {};
|
||||
const Status cleared = CheckJni(env);
|
||||
if (!cleared.ok()) status = cleared;
|
||||
}
|
||||
if (state_->protocol) env->DeleteGlobalRef(state_->protocol);
|
||||
if (state_->bus) env->DeleteGlobalRef(state_->bus);
|
||||
state_.reset();
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -1,7 +1,11 @@
|
||||
#include "runtime/roblox_platform_web_symbols.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/web_view_protocol_contract.h"
|
||||
#include "linker/linker.h"
|
||||
|
||||
namespace mocktail {
|
||||
@@ -14,6 +18,41 @@ Function Resolve(void* library, const char* name) {
|
||||
linker::ResolveSymbol(library, std::string(name)));
|
||||
}
|
||||
|
||||
// Reads the mapped initializer and follows its getter call only when both
|
||||
// match the code contract for this host's architecture. Both reads stay
|
||||
// inside libroblox.so's text segment: the initializer is longer than the scan
|
||||
// window, and the getter address comes from a verified call site.
|
||||
AcquireWebViewProtocolFn FindWebViewProtocolGetter(const void* initializer) {
|
||||
if (initializer == nullptr) return nullptr;
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(initializer);
|
||||
const std::string_view code(static_cast<const char*>(initializer),
|
||||
compat::kWebViewProtocolInitializerScanBytes);
|
||||
#if defined(__x86_64__)
|
||||
const std::optional<std::uintptr_t> getter =
|
||||
compat::FindWebViewProtocolGetterCallX86_64(code, address);
|
||||
const bool getter_matches =
|
||||
getter.has_value() &&
|
||||
compat::HasWebViewProtocolGetterContractX86_64(std::string_view(
|
||||
reinterpret_cast<const char*>(*getter),
|
||||
compat::kWebViewProtocolGetterBytesX86_64));
|
||||
#elif defined(__aarch64__)
|
||||
const std::optional<std::uintptr_t> getter =
|
||||
compat::FindWebViewProtocolGetterCallArm64(code, address);
|
||||
const bool getter_matches =
|
||||
getter.has_value() &&
|
||||
compat::HasWebViewProtocolGetterContractArm64(std::string_view(
|
||||
reinterpret_cast<const char*>(*getter),
|
||||
compat::kWebViewProtocolGetterBytesArm64));
|
||||
#else
|
||||
const std::optional<std::uintptr_t> getter;
|
||||
const bool getter_matches = false;
|
||||
(void)code;
|
||||
(void)address;
|
||||
#endif
|
||||
return getter_matches ? reinterpret_cast<AcquireWebViewProtocolFn>(*getter)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RobloxPlatformWebSymbols ResolveRobloxPlatformWebSymbols(
|
||||
@@ -46,6 +85,9 @@ RobloxPlatformWebSymbols ResolveRobloxPlatformWebSymbols(
|
||||
roblox_library,
|
||||
"Java_com_roblox_protocols_webview_WebViewProtocol_"
|
||||
"initializeAndroidWebViewProtocol");
|
||||
symbols.web_view.acquire_web_view_protocol = FindWebViewProtocolGetter(
|
||||
reinterpret_cast<const void*>(
|
||||
symbols.web_view.initialize_android_web_view_protocol));
|
||||
symbols.web_view.subscribe_raw = subscribe_raw;
|
||||
symbols.web_view.delete_connection = delete_connection;
|
||||
symbols.web_view.set_request_handler_raw =
|
||||
@@ -118,6 +160,24 @@ RobloxPlatformWebSymbols ResolveRobloxPlatformWebSymbols(
|
||||
roblox_library,
|
||||
"Java_com_roblox_universalapp_messagebus_MessageBus_"
|
||||
"callResponseHandlerRaw");
|
||||
using AccountStringFn = RobloxAccountProtocolSymbols::GetStringFn;
|
||||
constexpr char kAccountPrefix[] =
|
||||
"Java_com_roblox_universalapp_account_JNIAccountProtocol_";
|
||||
const auto account_getter = [&](const char* name) {
|
||||
return Resolve<AccountStringFn>(roblox_library,
|
||||
(std::string(kAccountPrefix) + name).c_str());
|
||||
};
|
||||
symbols.account.get_protocol_name = account_getter("getProtocolName");
|
||||
symbols.account.get_device_integrity_available_method_name =
|
||||
account_getter("getDeviceIntegrityAvailableMethodName");
|
||||
symbols.account.get_integrity_token_method_name =
|
||||
account_getter("getGetIntegrityTokenMethodName");
|
||||
symbols.account.get_support_key = account_getter("getSupportKey");
|
||||
symbols.account.get_token_key = account_getter("getTokenKey");
|
||||
symbols.account.get_result_key = account_getter("getResultKey");
|
||||
symbols.account.set_request_handler_raw =
|
||||
symbols.web_view.set_request_handler_raw;
|
||||
symbols.account.clear_request_handler = symbols.web_view.clear_request_handler;
|
||||
return symbols;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "runtime/roblox_presence_resolver.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
Status Invalid(std::string message) {
|
||||
return Status::Error(StatusCode::kInvalidArgument, std::move(message));
|
||||
}
|
||||
|
||||
size_t WriteToString(char* data, size_t size, size_t nmemb, void* userdata) {
|
||||
static_cast<std::string*>(userdata)->append(data, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Reads mocktail's own persisted session cookie from
|
||||
// $XDG_DATA_HOME/mocktail/auth/roblox.cookie (default
|
||||
// ~/.local/share/mocktail/auth/roblox.cookie), matching the format
|
||||
// mocktail's own auth code already writes there: a single line of the
|
||||
// form ".ROBLOSECURITY=<value>". Strips the prefix so callers get just
|
||||
// the bare value. Returns an empty string if no session is stored yet.
|
||||
std::string ReadStoredRoblosecurityCookie() {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home == nullptr || home[0] == '\0') {
|
||||
return std::string();
|
||||
}
|
||||
const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
|
||||
const std::string base =
|
||||
(xdg_data_home != nullptr && xdg_data_home[0] != '\0')
|
||||
? std::string(xdg_data_home)
|
||||
: std::string(home) + "/.local/share";
|
||||
const std::string path = base + "/mocktail/auth/roblox.cookie";
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return std::string();
|
||||
}
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
|
||||
line.pop_back();
|
||||
}
|
||||
constexpr std::string_view kPrefix = ".ROBLOSECURITY=";
|
||||
if (line.size() > kPrefix.size() &&
|
||||
line.compare(0, kPrefix.size(), kPrefix) == 0) {
|
||||
return line.substr(kPrefix.size());
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
Status ResolveFollowUserPlace(int64_t user_id, const std::string& auth_cookie,
|
||||
ResolvedFollowUserPlace* out) {
|
||||
if (out == nullptr || user_id <= 0) {
|
||||
return Invalid("Roblox presence lookup requires a positive userId");
|
||||
}
|
||||
if (auth_cookie.empty()) {
|
||||
return Invalid("Roblox presence lookup requires an authenticated session");
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl == nullptr) {
|
||||
return Invalid("Roblox presence lookup could not initialize a request");
|
||||
}
|
||||
|
||||
const nlohmann::json body = {{"userIds", {user_id}}};
|
||||
const std::string payload = body.dump();
|
||||
std::string response;
|
||||
|
||||
struct curl_slist* headers = nullptr;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
const std::string cookie_header = "Cookie: .ROBLOSECURITY=" + auth_cookie;
|
||||
headers = curl_slist_append(headers, cookie_header.c_str());
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL,
|
||||
"https://presence.roblox.com/v1/presence/users");
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteToString);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
|
||||
const CURLcode result = curl_easy_perform(curl);
|
||||
long http_status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (result != CURLE_OK) {
|
||||
return Invalid("Roblox presence lookup failed to connect");
|
||||
}
|
||||
if (http_status != 200) {
|
||||
return Invalid("Roblox presence lookup was rejected (status " +
|
||||
std::to_string(http_status) + ")");
|
||||
}
|
||||
|
||||
const nlohmann::json parsed = nlohmann::json::parse(
|
||||
response, /*cb=*/nullptr, /*allow_exceptions=*/false);
|
||||
if (parsed.is_discarded()) {
|
||||
return Invalid("Roblox presence lookup returned malformed JSON");
|
||||
}
|
||||
|
||||
if (!parsed.contains("userPresences") ||
|
||||
!parsed["userPresences"].is_array() ||
|
||||
parsed["userPresences"].empty()) {
|
||||
return Invalid("Roblox presence lookup returned no data for user");
|
||||
}
|
||||
|
||||
const auto& presence = parsed["userPresences"][0];
|
||||
// userPresenceType: 0 offline, 1 online, 2 in-game, 3 in-studio.
|
||||
const int presence_type = presence.value("userPresenceType", 0);
|
||||
if (presence_type != 2) {
|
||||
return Invalid("User is not currently in a joinable Roblox experience");
|
||||
}
|
||||
if (!presence.contains("placeId") || presence["placeId"].is_null()) {
|
||||
return Invalid("Roblox presence lookup did not include a placeId");
|
||||
}
|
||||
|
||||
out->place_id = presence.value("placeId", int64_t{0});
|
||||
if (presence.contains("gameId") && !presence["gameId"].is_null()) {
|
||||
out->game_instance_id = presence.value("gameId", std::string());
|
||||
}
|
||||
if (out->place_id <= 0) {
|
||||
return Invalid("Roblox presence lookup returned an invalid placeId");
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "runtime/environment.h"
|
||||
#include "webview/roblox_challenge_features.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
@@ -401,6 +402,39 @@ Status ParseRobloxCaptchaOpenJson(const std::string& json,
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
bool IsRobloxWebViewChallengeSuccess(std::string_view command) {
|
||||
if (command.empty() || command.size() > kMaximumRobloxWebViewJsonBytes) {
|
||||
return false;
|
||||
}
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(command.begin(), command.end(), nullptr, false);
|
||||
const auto string_field = [](const nlohmann::json& object,
|
||||
const char* key) -> const std::string* {
|
||||
const auto field = object.find(key);
|
||||
return field != object.end() && field->is_string()
|
||||
? &field->get_ref<const std::string&>()
|
||||
: nullptr;
|
||||
};
|
||||
const auto object_field = [](const nlohmann::json& object,
|
||||
const char* key) -> const nlohmann::json* {
|
||||
const auto field = object.find(key);
|
||||
return field != object.end() && field->is_object() ? &*field : nullptr;
|
||||
};
|
||||
if (!document.is_object()) {
|
||||
return false;
|
||||
}
|
||||
const std::string* module = string_field(document, "moduleID");
|
||||
const std::string* function = string_field(document, "functionName");
|
||||
const nlohmann::json* outer = object_field(document, "params");
|
||||
const nlohmann::json* navigation =
|
||||
outer != nullptr ? object_field(*outer, "params") : nullptr;
|
||||
const std::string* feature =
|
||||
navigation != nullptr ? string_field(*navigation, "feature") : nullptr;
|
||||
return module != nullptr && *module == "Navigation" && function != nullptr &&
|
||||
*function == "navigateToFeature" && feature != nullptr &&
|
||||
webview::IsRobloxChallengeSolvedFeature(*feature);
|
||||
}
|
||||
|
||||
Status ParseRobloxProfileViewUrl(const std::string& url,
|
||||
RobloxWebViewOpenRequest* request) {
|
||||
if (request == nullptr) {
|
||||
@@ -911,6 +945,20 @@ Status RobloxWebViewBridge::Initialize() {
|
||||
// handler and window subscriptions are installed before the native Android
|
||||
// protocol is initialized. Calling the native initializer earlier loses
|
||||
// messages that it can publish synchronously during startup.
|
||||
// Roblox's Android app start holds a WebViewProtocol handle for the process
|
||||
// lifetime. Without one, the protocol is deleted as soon as each caller
|
||||
// releases it, and page callbacks wait for the next Lua WebView call to
|
||||
// recreate it. Taking it before the native initializer keeps the protocol
|
||||
// that initializer configures.
|
||||
if (symbols_.acquire_web_view_protocol != nullptr) {
|
||||
if (web_view_protocol_handle_.holder() == nullptr) {
|
||||
web_view_protocol_handle_ = symbols_.acquire_web_view_protocol();
|
||||
}
|
||||
} else {
|
||||
std::fprintf(stderr,
|
||||
" [webview] WebViewProtocol getter not recognized; page "
|
||||
"callbacks may wait for the next Roblox WebView call\n");
|
||||
}
|
||||
jclass initialization_class = env->FindClass(kWebViewProtocolClass);
|
||||
if (initialization_class != nullptr) {
|
||||
symbols_.initialize_android_web_view_protocol(env, initialization_class);
|
||||
@@ -1266,6 +1314,7 @@ Status RobloxWebViewBridge::DispatchOpenRequest(
|
||||
exit_context->target->browser_login_fallback = browser_login_fallback;
|
||||
}
|
||||
}
|
||||
request.close_when_challenge_solved = source == OpenSource::kCaptcha;
|
||||
if (status.ok()) {
|
||||
status = sink_.dispatch_open(
|
||||
sink_.context, request,
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
@@ -438,8 +440,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
const std::filesystem::path library = canonical_root / "libroblox.so";
|
||||
const std::filesystem::path package_root = canonical_root / "sober_apk";
|
||||
const std::filesystem::path base_apk = package_root / "base.apk";
|
||||
const std::filesystem::path x86_64_split_apk =
|
||||
package_root / "split_config.x86_64.apk";
|
||||
const std::filesystem::path arch_split_apk =
|
||||
package_root / compat::kGuestSplitApkFile;
|
||||
const std::filesystem::path assets_root = canonical_root / "assets";
|
||||
const std::filesystem::path assets = canonical_root / "assets/content";
|
||||
const std::filesystem::file_status library_status =
|
||||
@@ -460,7 +462,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
for (const auto& [path, name] :
|
||||
std::array<std::pair<std::filesystem::path, const char*>, 2>{
|
||||
std::pair{base_apk, "base.apk"},
|
||||
std::pair{x86_64_split_apk, "split_config.x86_64.apk"}}) {
|
||||
std::pair{arch_split_apk,
|
||||
compat::kGuestSplitApkFile.data()}}) {
|
||||
const std::filesystem::file_status status =
|
||||
std::filesystem::symlink_status(path, filesystem_error);
|
||||
if (filesystem_error || !std::filesystem::is_regular_file(status) ||
|
||||
@@ -489,7 +492,7 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const {
|
||||
result.root = canonical_root;
|
||||
result.roblox_library = library;
|
||||
result.base_apk = base_apk;
|
||||
result.x86_64_split_apk = x86_64_split_apk;
|
||||
result.arch_split_apk = arch_split_apk;
|
||||
result.assets_content = assets;
|
||||
|
||||
const auto compatibility_manifest =
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "runtime/webview_helper_launcher.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <curl/curl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
@@ -21,6 +24,7 @@
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
@@ -38,6 +42,8 @@ struct WebViewHelperProcess::State {
|
||||
std::mutex control_mutex;
|
||||
int control_descriptor = -1;
|
||||
std::deque<WebViewHelperEvent> pending_events;
|
||||
std::string receive_buffer;
|
||||
bool tcp = false;
|
||||
bool ready = false;
|
||||
|
||||
~State() {
|
||||
@@ -67,7 +73,36 @@ class SensitiveStringGuard final {
|
||||
WebViewHelperProcess::WebViewHelperProcess(std::shared_ptr<State> state)
|
||||
: state_(std::move(state)) {}
|
||||
|
||||
bool WebViewHelperProcess::running() const { return process_id() > 0; }
|
||||
bool WebViewHelperProcess::running() const {
|
||||
if (state_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (!state_->tcp) {
|
||||
return process_id() > 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(state_->control_mutex);
|
||||
if (state_->control_descriptor < 0 ||
|
||||
state_->child.load(std::memory_order_acquire) <= 0) {
|
||||
return false;
|
||||
}
|
||||
pollfd probe = {state_->control_descriptor, POLLIN, 0};
|
||||
if (poll(&probe, 1, 0) > 0) {
|
||||
if ((probe.revents & (POLLHUP | POLLERR | POLLNVAL)) != 0) {
|
||||
state_->child.store(-1, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
if ((probe.revents & POLLIN) != 0) {
|
||||
char peek;
|
||||
const ssize_t count = recv(state_->control_descriptor, &peek, 1,
|
||||
MSG_PEEK | MSG_DONTWAIT);
|
||||
if (count == 0) {
|
||||
state_->child.store(-1, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pid_t WebViewHelperProcess::process_id() const {
|
||||
return state_ != nullptr ? state_->child.load(std::memory_order_acquire) : -1;
|
||||
@@ -135,6 +170,114 @@ private:
|
||||
char *value_ = nullptr;
|
||||
};
|
||||
|
||||
bool HostIsFreeBsd() {
|
||||
char buffer[512] = {};
|
||||
std::ifstream file("/proc/version", std::ios::binary);
|
||||
file.read(buffer, sizeof(buffer) - 1);
|
||||
const std::string_view text(buffer, static_cast<std::size_t>(file.gcount()));
|
||||
return text.find("FreeBSD") != std::string_view::npos ||
|
||||
text.find("freebsd.org") != std::string_view::npos;
|
||||
}
|
||||
|
||||
std::string FramePacket(std::string_view packet) {
|
||||
std::string out(4, '\0');
|
||||
const std::size_t size = packet.size();
|
||||
out[0] = static_cast<char>((size >> 24) & 0xff);
|
||||
out[1] = static_cast<char>((size >> 16) & 0xff);
|
||||
out[2] = static_cast<char>((size >> 8) & 0xff);
|
||||
out[3] = static_cast<char>(size & 0xff);
|
||||
out.append(packet);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ExtractFrame(std::string *buffer, std::string *frame) {
|
||||
if (buffer->size() < 4) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t size =
|
||||
(static_cast<std::size_t>(static_cast<unsigned char>((*buffer)[0])) << 24) |
|
||||
(static_cast<std::size_t>(static_cast<unsigned char>((*buffer)[1])) << 16) |
|
||||
(static_cast<std::size_t>(static_cast<unsigned char>((*buffer)[2])) << 8) |
|
||||
static_cast<std::size_t>(static_cast<unsigned char>((*buffer)[3]));
|
||||
if (size > kMaximumWebViewEventPacketBytes) {
|
||||
buffer->clear();
|
||||
return false;
|
||||
}
|
||||
if (buffer->size() < 4 + size) {
|
||||
return false;
|
||||
}
|
||||
frame->assign(*buffer, 4, size);
|
||||
buffer->erase(0, 4 + size);
|
||||
return true;
|
||||
}
|
||||
|
||||
int ConnectToFreeBsdHost() {
|
||||
const int descriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (descriptor < 0) {
|
||||
return -1;
|
||||
}
|
||||
fcntl(descriptor, F_SETFD, FD_CLOEXEC);
|
||||
sockaddr_in address{};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(2137);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
int enabled = 1;
|
||||
setsockopt(descriptor, IPPROTO_TCP, TCP_NODELAY, &enabled, sizeof(enabled));
|
||||
if (connect(descriptor, reinterpret_cast<sockaddr *>(&address),
|
||||
sizeof(address)) != 0) {
|
||||
close(descriptor);
|
||||
return -1;
|
||||
}
|
||||
const int flags = fcntl(descriptor, F_GETFL);
|
||||
if (flags < 0 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) != 0) {
|
||||
close(descriptor);
|
||||
return -1;
|
||||
}
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
ssize_t ReceivePacket(WebViewHelperProcess::State *state,
|
||||
std::string *packet) {
|
||||
if (!state->tcp) {
|
||||
packet->assign(kMaximumWebViewEventPacketBytes, '\0');
|
||||
const ssize_t count = recv(state->control_descriptor, packet->data(),
|
||||
packet->size(), MSG_DONTWAIT | MSG_TRUNC);
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
return -1;
|
||||
}
|
||||
if (count < 0) {
|
||||
return -2;
|
||||
}
|
||||
if (count > 0 && static_cast<std::size_t>(count) <= packet->size()) {
|
||||
packet->resize(static_cast<std::size_t>(count));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
while (true) {
|
||||
if (ExtractFrame(&state->receive_buffer, packet)) {
|
||||
return static_cast<ssize_t>(packet->size());
|
||||
}
|
||||
char chunk[4096];
|
||||
const ssize_t count = read(state->control_descriptor, chunk, sizeof(chunk));
|
||||
if (count < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
return -1;
|
||||
}
|
||||
if (count < 0) {
|
||||
return -2;
|
||||
}
|
||||
if (count == 0) {
|
||||
return 0;
|
||||
}
|
||||
state->receive_buffer.append(chunk, static_cast<std::size_t>(count));
|
||||
if (state->receive_buffer.size() > kMaximumWebViewEventPacketBytes * 2) {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsRobloxHost(std::string_view host) {
|
||||
constexpr std::string_view kRoot = "roblox.com";
|
||||
std::string normalized(host);
|
||||
@@ -258,13 +401,39 @@ bool SendControlCommand(
|
||||
std::lock_guard<std::mutex> lock(state->control_mutex);
|
||||
if (state->child.load(std::memory_order_acquire) > 0 &&
|
||||
state->control_descriptor >= 0) {
|
||||
ssize_t count = -1;
|
||||
do {
|
||||
// FreeBSD requires an explicit record boundary for SOCK_SEQPACKET.
|
||||
count = send(state->control_descriptor, packet.data(), packet.size(),
|
||||
MSG_DONTWAIT | MSG_NOSIGNAL | MSG_EOR);
|
||||
} while (count < 0 && errno == EINTR);
|
||||
sent = count == static_cast<ssize_t>(packet.size());
|
||||
if (state->tcp) {
|
||||
const std::string framed = FramePacket(packet);
|
||||
std::size_t offset = 0;
|
||||
sent = true;
|
||||
while (offset < framed.size()) {
|
||||
const ssize_t count =
|
||||
send(state->control_descriptor, framed.data() + offset,
|
||||
framed.size() - offset, MSG_NOSIGNAL);
|
||||
if (count < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
pollfd writable = {state->control_descriptor, POLLOUT, 0};
|
||||
if (poll(&writable, 1, 1000) <= 0) {
|
||||
sent = false;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (count <= 0) {
|
||||
sent = false;
|
||||
break;
|
||||
}
|
||||
offset += static_cast<std::size_t>(count);
|
||||
}
|
||||
} else {
|
||||
ssize_t count = -1;
|
||||
do {
|
||||
count = send(state->control_descriptor, packet.data(), packet.size(),
|
||||
MSG_DONTWAIT | MSG_NOSIGNAL | MSG_EOR);
|
||||
} while (count < 0 && errno == EINTR);
|
||||
sent = count == static_cast<ssize_t>(packet.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fill(packet.begin(), packet.end(), '\0');
|
||||
@@ -740,31 +909,41 @@ bool WebViewHelperProcess::WaitUntilReady(
|
||||
if (remaining.count() <= 0) {
|
||||
break;
|
||||
}
|
||||
std::string packet;
|
||||
bool closed = false;
|
||||
while (!state_->ready) {
|
||||
const ssize_t count = ReceivePacket(state_.get(), &packet);
|
||||
if (count == 0 || count == -2) {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
if (count < 0) {
|
||||
break;
|
||||
}
|
||||
WebViewHelperEvent event;
|
||||
if (!DecodeWebViewHelperEventPacket(packet, &event)) {
|
||||
continue;
|
||||
}
|
||||
if (event.type == WebViewHelperEventType::kReady) {
|
||||
state_->ready = true;
|
||||
} else {
|
||||
state_->pending_events.push_back(std::move(event));
|
||||
}
|
||||
}
|
||||
if (closed || state_->ready) {
|
||||
break;
|
||||
}
|
||||
pollfd descriptor = {state_->control_descriptor, POLLIN, 0};
|
||||
int poll_status = -1;
|
||||
do {
|
||||
poll_status = poll(&descriptor, 1, static_cast<int>(remaining.count()));
|
||||
} while (poll_status < 0 && errno == EINTR);
|
||||
if (poll_status <= 0 || (descriptor.revents & POLLIN) == 0) {
|
||||
if (poll_status < 0) {
|
||||
break;
|
||||
}
|
||||
std::string packet(kMaximumWebViewEventPacketBytes, '\0');
|
||||
const ssize_t count = recv(state_->control_descriptor, packet.data(),
|
||||
packet.size(), MSG_DONTWAIT | MSG_TRUNC);
|
||||
if (count <= 0 || static_cast<std::size_t>(count) > packet.size()) {
|
||||
if (poll_status > 0 && (descriptor.revents & (POLLERR | POLLNVAL)) != 0) {
|
||||
break;
|
||||
}
|
||||
WebViewHelperEvent event;
|
||||
if (!DecodeWebViewHelperEventPacket(
|
||||
std::string_view(packet.data(), static_cast<std::size_t>(count)),
|
||||
&event)) {
|
||||
continue;
|
||||
}
|
||||
if (event.type == WebViewHelperEventType::kReady) {
|
||||
state_->ready = true;
|
||||
} else {
|
||||
state_->pending_events.push_back(std::move(event));
|
||||
}
|
||||
}
|
||||
return state_->ready;
|
||||
}
|
||||
@@ -782,26 +961,17 @@ bool WebViewHelperProcess::DrainEvents(
|
||||
events->push_back(std::move(state_->pending_events.front()));
|
||||
state_->pending_events.pop_front();
|
||||
}
|
||||
std::string packet(kMaximumWebViewEventPacketBytes, '\0');
|
||||
std::string packet;
|
||||
while (true) {
|
||||
ssize_t count = recv(state_->control_descriptor, packet.data(),
|
||||
packet.size(), MSG_DONTWAIT | MSG_TRUNC);
|
||||
if (count < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
const ssize_t count = ReceivePacket(state_.get(), &packet);
|
||||
if (count == -1 || count == 0) {
|
||||
return true;
|
||||
}
|
||||
if (count <= 0) {
|
||||
return count == 0;
|
||||
}
|
||||
if (static_cast<std::size_t>(count) > packet.size()) {
|
||||
continue;
|
||||
if (count < 0) {
|
||||
return false;
|
||||
}
|
||||
WebViewHelperEvent event;
|
||||
if (DecodeWebViewHelperEventPacket(
|
||||
std::string_view(packet.data(), static_cast<std::size_t>(count)),
|
||||
&event)) {
|
||||
if (DecodeWebViewHelperEventPacket(packet, &event)) {
|
||||
if (event.type == WebViewHelperEventType::kReady) {
|
||||
state_->ready = true;
|
||||
} else {
|
||||
@@ -819,6 +989,9 @@ bool WebViewHelperProcess::RequestClose() const {
|
||||
if (SendControlCommand(state_, WebViewHelperControlOperation::kClose, {})) {
|
||||
return true;
|
||||
}
|
||||
if (state_->tcp) {
|
||||
return false;
|
||||
}
|
||||
const pid_t current_child = process_id();
|
||||
if (current_child <= 0) {
|
||||
return true;
|
||||
@@ -835,6 +1008,26 @@ WebViewHelperLaunchResult LaunchWebViewHelper(
|
||||
if (!NormalizeWebViewUrl(url, &normalized_url, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
if (HostIsFreeBsd()) {
|
||||
const int descriptor = ConnectToFreeBsdHost();
|
||||
if (descriptor < 0) {
|
||||
result.error = "cannot connect to the FreeBSD webview host";
|
||||
return result;
|
||||
}
|
||||
auto tcp_state = std::make_shared<WebViewHelperProcess::State>();
|
||||
tcp_state->tcp = true;
|
||||
tcp_state->control_descriptor = descriptor;
|
||||
tcp_state->child.store(1, std::memory_order_release);
|
||||
result.process = std::shared_ptr<WebViewHelperProcess>(
|
||||
new WebViewHelperProcess(std::move(tcp_state)));
|
||||
result.spawned = true;
|
||||
result.process_id = 1;
|
||||
if (!result.process->LoadUrl(normalized_url)) {
|
||||
result.spawned = false;
|
||||
result.error = "cannot send the initial URL to the FreeBSD webview host";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::error_code filesystem_error;
|
||||
const std::filesystem::file_status helper_status =
|
||||
std::filesystem::symlink_status(helper, filesystem_error);
|
||||
|
||||
+25
-13
@@ -28,6 +28,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/android_manifest.h"
|
||||
#include "update/apk_signature.h"
|
||||
#include "update/payload_integrity.h"
|
||||
@@ -173,10 +174,15 @@ bool EndsWith(std::string_view value, std::string_view suffix) {
|
||||
struct ApkCandidate {
|
||||
std::filesystem::path path;
|
||||
AndroidManifestIdentity identity;
|
||||
bool has_x86_64_library = false;
|
||||
bool has_guest_library = false;
|
||||
bool has_assets = false;
|
||||
};
|
||||
|
||||
std::string GuestLibEntryName() {
|
||||
return std::string(compat::kGuestLibApkEntryPrefix) +
|
||||
std::string(compat::kGuestAbi) + "/libroblox.so";
|
||||
}
|
||||
|
||||
bool CollectCandidates(const std::vector<std::filesystem::path>& archives,
|
||||
const std::filesystem::path& directory,
|
||||
std::vector<std::filesystem::path>* candidates,
|
||||
@@ -250,7 +256,7 @@ bool InspectCandidates(const std::vector<std::filesystem::path>& paths,
|
||||
ApkCandidate candidate;
|
||||
candidate.path = path;
|
||||
candidate.identity = std::move(identity);
|
||||
candidate.has_x86_64_library = HasEntry(list, "lib/x86_64/libroblox.so");
|
||||
candidate.has_guest_library = HasEntry(list, GuestLibEntryName());
|
||||
candidate.has_assets = std::any_of(
|
||||
list.entries.begin(), list.entries.end(), [](const ZipEntry& entry) {
|
||||
return entry.name.size() > 7 && entry.name.substr(0, 7) == "assets/";
|
||||
@@ -314,7 +320,8 @@ std::vector<std::string> CommonTrustedCertificates(
|
||||
const ApkSignatureResult split_signature =
|
||||
base == split ? base_signature : VerifyApkSignature(split);
|
||||
if (!split_signature) {
|
||||
*error = "x86_64 APK signature: " + split_signature.error;
|
||||
*error = std::string(compat::kGuestAbi) +
|
||||
" APK signature: " + split_signature.error;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> accepted;
|
||||
@@ -356,9 +363,11 @@ bool ValidateElf(const std::filesystem::path& path, std::string* build_id,
|
||||
}
|
||||
GElf_Ehdr header{};
|
||||
if (gelf_getehdr(elf.get(), &header) == nullptr ||
|
||||
header.e_machine != EM_X86_64 || gelf_getclass(elf.get()) != ELFCLASS64) {
|
||||
header.e_machine != compat::kGuestElfMachine ||
|
||||
gelf_getclass(elf.get()) != ELFCLASS64) {
|
||||
close_elf();
|
||||
*error = "libroblox.so is not an x86-64 ELF";
|
||||
*error = "libroblox.so is not a " + std::string(compat::kGuestAbi) +
|
||||
" ELF";
|
||||
return false;
|
||||
}
|
||||
std::size_t section_count = 0;
|
||||
@@ -464,16 +473,18 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
base = &candidate;
|
||||
}
|
||||
}
|
||||
const std::string guest_split_name(compat::kGuestApkSplitName);
|
||||
for (const ApkCandidate& candidate : candidates) {
|
||||
if (candidate.has_x86_64_library &&
|
||||
(candidate.identity.split_name == "config.x86_64" ||
|
||||
if (candidate.has_guest_library &&
|
||||
(candidate.identity.split_name == guest_split_name ||
|
||||
candidate.identity.split_name.empty()) &&
|
||||
split == nullptr) {
|
||||
split = &candidate;
|
||||
}
|
||||
}
|
||||
if (base == nullptr || split == nullptr) {
|
||||
result.error = "no matching Roblox base and x86_64 APK pair was found";
|
||||
result.error = "no matching Roblox base and " +
|
||||
std::string(compat::kGuestAbi) + " APK pair was found";
|
||||
return result;
|
||||
}
|
||||
const std::set<std::string> trusted =
|
||||
@@ -488,12 +499,13 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
if (filesystem_error ||
|
||||
!CopyRegular(base->path, prepared / "sober_apk/base.apk",
|
||||
&result.error) ||
|
||||
!CopyRegular(split->path, prepared / "sober_apk/split_config.x86_64.apk",
|
||||
!CopyRegular(split->path,
|
||||
prepared / "sober_apk" / compat::kGuestSplitApkFile,
|
||||
&result.error)) {
|
||||
if (result.error.empty()) result.error = "cannot create prepared payload";
|
||||
return result;
|
||||
}
|
||||
if (!ExtractZipEntry(split->path, "lib/x86_64/libroblox.so",
|
||||
if (!ExtractZipEntry(split->path, GuestLibEntryName(),
|
||||
prepared / "libroblox.so", kMaximumApkBytes,
|
||||
&result.error)) {
|
||||
return result;
|
||||
@@ -527,7 +539,7 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
const std::string base_hash =
|
||||
HashRegularFile(prepared / "sober_apk/base.apk", &hash_error);
|
||||
const std::string split_hash = HashRegularFile(
|
||||
prepared / "sober_apk/split_config.x86_64.apk", &hash_error);
|
||||
prepared / "sober_apk" / compat::kGuestSplitApkFile, &hash_error);
|
||||
if (!hash_error.empty()) {
|
||||
result.error = hash_error;
|
||||
return result;
|
||||
@@ -537,12 +549,12 @@ PreparedPayload PreparePayloadFromArchives(
|
||||
{"package", "com.roblox.client"},
|
||||
{"version_name", expected.version_name},
|
||||
{"version_code", expected.version_code},
|
||||
{"abi", "x86_64"},
|
||||
{"abi", std::string(compat::kGuestAbi)},
|
||||
{"elf_build_id", build_id},
|
||||
{"sha256",
|
||||
{{"libroblox", library_hash},
|
||||
{"base_apk", base_hash},
|
||||
{"x86_64_split_apk", split_hash}}},
|
||||
{std::string(compat::kGuestSplitApkHashKey), split_hash}}},
|
||||
{"signing_certificates_sha256", accepted},
|
||||
{"assets",
|
||||
{{"file_count", asset_count_verified},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/http_download.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -199,7 +200,8 @@ std::vector<std::string> ParseApkPureExactDownloadUrls(
|
||||
}
|
||||
|
||||
ProviderVersion ApkPureProvider::CheckLatest() const {
|
||||
const HttpBytesResult metadata = FetchMetadata("x86_64");
|
||||
const HttpBytesResult metadata =
|
||||
FetchMetadata(std::string(compat::kGuestAbi));
|
||||
if (!metadata) return ProviderVersion{{}, 0, metadata.error};
|
||||
return ParseApkPureLatestMetadata(metadata.bytes);
|
||||
}
|
||||
@@ -223,7 +225,7 @@ ProviderDownloadResult ApkPureProvider::DownloadExact(
|
||||
result.error = "APKPure output directory must exist and be empty";
|
||||
return result;
|
||||
}
|
||||
HttpBytesResult metadata = FetchMetadata("x86_64");
|
||||
HttpBytesResult metadata = FetchMetadata(std::string(compat::kGuestAbi));
|
||||
if (!metadata) {
|
||||
result.error = metadata.error;
|
||||
return result;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/payload_integrity.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -260,7 +261,7 @@ std::string PayloadRuntimeFingerprint(
|
||||
evidence.append("base_apk=")
|
||||
.append(payload.metadata.base_apk_sha256)
|
||||
.append("\n");
|
||||
evidence.append("x86_64_split=")
|
||||
evidence.append(std::string(compat::kGuestAbi) + "_split=")
|
||||
.append(payload.metadata.split_apk_sha256)
|
||||
.append("\n");
|
||||
evidence.append("assets=")
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <cerrno>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
namespace {
|
||||
@@ -70,17 +73,38 @@ CompatibilityCatalogResult LoadCompatibilityCatalog(
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(contents, nullptr, false, true);
|
||||
if (document.is_discarded() || !document.is_object() ||
|
||||
document.value("schema_version", 0) != 1 ||
|
||||
!document.contains("profiles") || !document["profiles"].is_array()) {
|
||||
!document.contains("schema_version") ||
|
||||
!document["schema_version"].is_number_integer() ||
|
||||
document["schema_version"] != 1 || !document.contains("profiles") ||
|
||||
!document["profiles"].is_array()) {
|
||||
result.error = "compatibility manifest has an unsupported schema";
|
||||
return result;
|
||||
}
|
||||
for (const nlohmann::json& profile : document["profiles"]) {
|
||||
if (!profile.is_object() || profile.value("status", "") != "supported" ||
|
||||
if (!profile.is_object()) continue;
|
||||
if ((profile.contains("status") && !profile["status"].is_string()) ||
|
||||
(profile.contains("default_allowed") &&
|
||||
!profile["default_allowed"].is_boolean()) ||
|
||||
(profile.contains("allow_legacy_binary_patches") &&
|
||||
!profile["allow_legacy_binary_patches"].is_boolean())) {
|
||||
result.error = "compatibility profile flags have invalid types";
|
||||
return result;
|
||||
}
|
||||
if (profile.value("status", "") != "supported" ||
|
||||
!profile.value("default_allowed", false) ||
|
||||
profile.value("allow_legacy_binary_patches", true)) {
|
||||
continue;
|
||||
}
|
||||
const auto declared_abi = profile.find("abi");
|
||||
if (declared_abi != profile.end() && !declared_abi->is_string()) {
|
||||
result.error = "supported compatibility profile is incomplete";
|
||||
return result;
|
||||
}
|
||||
const std::string_view profile_abi =
|
||||
declared_abi == profile.end()
|
||||
? std::string_view("x86_64")
|
||||
: std::string_view(declared_abi->get_ref<const std::string&>());
|
||||
if (profile_abi != compat::kGuestAbi) continue;
|
||||
if (!profile.contains("version_name") ||
|
||||
!profile["version_name"].is_string() ||
|
||||
!profile.contains("version_code") ||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "compat/elf_build_id.h"
|
||||
#include "compat/fmod_output_device_contract.h"
|
||||
#include "compat/guest_abi.h"
|
||||
#include "update/payload_integrity.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -222,9 +223,10 @@ class ElfImage final {
|
||||
if (elf_ == nullptr || elf_kind(elf_) != ELF_K_ELF ||
|
||||
gelf_getehdr(elf_, &header) == nullptr ||
|
||||
gelf_getclass(elf_) != ELFCLASS64 || header.e_type != ET_DYN ||
|
||||
header.e_machine != EM_X86_64 || header.e_version != EV_CURRENT ||
|
||||
header.e_machine != compat::kGuestElfMachine ||
|
||||
header.e_version != EV_CURRENT ||
|
||||
header.e_ident[EI_DATA] != ELFDATA2LSB) {
|
||||
*error = "candidate must be a little-endian x86-64 ET_DYN ELF";
|
||||
*error = "candidate must be a little-endian guest-abi ET_DYN ELF";
|
||||
return false;
|
||||
}
|
||||
std::size_t program_count = 0;
|
||||
@@ -1901,7 +1903,7 @@ std::optional<Json> LoadCandidateMetadata(const std::filesystem::path& root,
|
||||
metadata->contains("sha256") ? &(*metadata)["sha256"] : nullptr;
|
||||
if (metadata->value("schema_version", 0) != 1 ||
|
||||
metadata->value("package", "") != "com.roblox.client" ||
|
||||
metadata->value("abi", "") != "x86_64" || version_name.empty() ||
|
||||
metadata->value("abi", "") != compat::kGuestAbi || version_name.empty() ||
|
||||
version_code == 0 ||
|
||||
metadata->value("elf_build_id", "") != candidate.build_id() ||
|
||||
hashes == nullptr || !hashes->is_object() ||
|
||||
|
||||
+3
-1
@@ -145,8 +145,10 @@ mocktail::update::UpdatePaths ResolvePaths() {
|
||||
runtime = public_runtime;
|
||||
}
|
||||
}
|
||||
paths.runtime_binary =
|
||||
paths.runtime_launcher =
|
||||
Environment("MOCKTAIL_UPDATE_CANARY_BIN").value_or(runtime.string());
|
||||
paths.runtime_binary = Environment("MOCKTAIL_UPDATE_RUNTIME_BINARY")
|
||||
.value_or(paths.runtime_launcher.string());
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "compat/guest_abi.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
namespace {
|
||||
|
||||
@@ -291,7 +294,7 @@ PayloadIntegrityResult InspectPreparedPayload(
|
||||
for (const std::filesystem::path& relative : {
|
||||
std::filesystem::path("libroblox.so"),
|
||||
std::filesystem::path("sober_apk/base.apk"),
|
||||
std::filesystem::path("sober_apk/split_config.x86_64.apk"),
|
||||
std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
|
||||
std::filesystem::path("roblox_payload.json"),
|
||||
}) {
|
||||
const auto status =
|
||||
@@ -320,14 +323,35 @@ PayloadIntegrityResult InspectPreparedPayload(
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(contents, nullptr, false, true);
|
||||
if (document.is_discarded() || !document.is_object() ||
|
||||
document.value("schema_version", 0) != 1 ||
|
||||
!document.contains("version_code") ||
|
||||
!document.contains("schema_version") ||
|
||||
!document["schema_version"].is_number_integer() ||
|
||||
document["schema_version"] != 1 || !document.contains("version_code") ||
|
||||
!document["version_code"].is_number_unsigned() ||
|
||||
!document.contains("sha256") || !document["sha256"].is_object() ||
|
||||
!document.contains("assets") || !document["assets"].is_object()) {
|
||||
result.error = "payload metadata schema is invalid";
|
||||
return result;
|
||||
}
|
||||
for (const char* field : {"package", "version_name", "elf_build_id"}) {
|
||||
if (!document.contains(field) || !document[field].is_string()) {
|
||||
result.error = "payload metadata identity or hashes are invalid";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
for (const std::string& field :
|
||||
{std::string("libroblox"), std::string("base_apk"),
|
||||
std::string(compat::kGuestSplitApkHashKey)}) {
|
||||
if (!document["sha256"].contains(field) ||
|
||||
!document["sha256"][field].is_string()) {
|
||||
result.error = "payload metadata identity or hashes are invalid";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!document["assets"].contains("sha256_tree") ||
|
||||
!document["assets"]["sha256_tree"].is_string()) {
|
||||
result.error = "payload metadata identity or hashes are invalid";
|
||||
return result;
|
||||
}
|
||||
result.metadata.package_name = document.value("package", "");
|
||||
result.metadata.version_name = document.value("version_name", "");
|
||||
result.metadata.version_code = document["version_code"].get<std::uint64_t>();
|
||||
@@ -335,9 +359,11 @@ PayloadIntegrityResult InspectPreparedPayload(
|
||||
result.metadata.library_sha256 = document["sha256"].value("libroblox", "");
|
||||
result.metadata.base_apk_sha256 = document["sha256"].value("base_apk", "");
|
||||
result.metadata.split_apk_sha256 =
|
||||
document["sha256"].value("x86_64_split_apk", "");
|
||||
document["sha256"].value(std::string(compat::kGuestSplitApkHashKey), "");
|
||||
if (!document["assets"].contains("file_count") ||
|
||||
!document["assets"]["file_count"].is_number_unsigned()) {
|
||||
!document["assets"]["file_count"].is_number_unsigned() ||
|
||||
document["assets"]["file_count"] >
|
||||
std::numeric_limits<std::size_t>::max()) {
|
||||
result.error = "payload metadata asset count is invalid";
|
||||
return result;
|
||||
}
|
||||
@@ -368,7 +394,8 @@ PayloadIntegrityResult VerifyPreparedPayload(
|
||||
const std::array<std::pair<std::filesystem::path, std::string>, 3> files = {{
|
||||
{"libroblox.so", result.metadata.library_sha256},
|
||||
{"sober_apk/base.apk", result.metadata.base_apk_sha256},
|
||||
{"sober_apk/split_config.x86_64.apk", result.metadata.split_apk_sha256},
|
||||
{std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile,
|
||||
result.metadata.split_apk_sha256},
|
||||
}};
|
||||
for (const auto& [relative, expected] : files) {
|
||||
const std::string actual =
|
||||
|
||||
+157
-36
@@ -111,22 +111,32 @@ std::string ReadRegular(const std::filesystem::path& path, std::size_t maximum,
|
||||
|
||||
bool WriteAtomic(const std::filesystem::path& root,
|
||||
const std::filesystem::path& destination,
|
||||
std::string_view contents, std::string* error) {
|
||||
std::string_view contents, std::string* error,
|
||||
const ManifestWriteFault& fault = {}) {
|
||||
static std::uint64_t generation = 0;
|
||||
const std::filesystem::path temporary =
|
||||
root / (".manifest-" + std::to_string(getpid()) + "-" +
|
||||
std::to_string(++generation));
|
||||
const auto fail = [&](ManifestWritePhase phase) {
|
||||
if (!fault || !fault(destination, phase)) return false;
|
||||
errno = EIO;
|
||||
return true;
|
||||
};
|
||||
const int descriptor =
|
||||
open(temporary.c_str(),
|
||||
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644);
|
||||
fail(ManifestWritePhase::kCreate)
|
||||
? -1
|
||||
: open(temporary.c_str(),
|
||||
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644);
|
||||
if (descriptor < 0) {
|
||||
*error = "cannot create atomic payload manifest";
|
||||
return false;
|
||||
}
|
||||
std::size_t offset = 0;
|
||||
while (offset < contents.size()) {
|
||||
const ssize_t bytes =
|
||||
write(descriptor, contents.data() + offset, contents.size() - offset);
|
||||
const ssize_t bytes = fail(ManifestWritePhase::kWrite)
|
||||
? -1
|
||||
: write(descriptor, contents.data() + offset,
|
||||
contents.size() - offset);
|
||||
if (bytes < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(descriptor);
|
||||
@@ -137,7 +147,8 @@ bool WriteAtomic(const std::filesystem::path& root,
|
||||
}
|
||||
offset += static_cast<std::size_t>(bytes);
|
||||
}
|
||||
const bool synced = fsync(descriptor) == 0;
|
||||
const bool synced =
|
||||
!fail(ManifestWritePhase::kFileFsync) && fsync(descriptor) == 0;
|
||||
close(descriptor);
|
||||
if (!synced) {
|
||||
std::error_code ignored;
|
||||
@@ -146,7 +157,11 @@ bool WriteAtomic(const std::filesystem::path& root,
|
||||
return false;
|
||||
}
|
||||
std::error_code filesystem_error;
|
||||
std::filesystem::rename(temporary, destination, filesystem_error);
|
||||
if (fail(ManifestWritePhase::kRename)) {
|
||||
filesystem_error = std::make_error_code(std::errc::io_error);
|
||||
} else {
|
||||
std::filesystem::rename(temporary, destination, filesystem_error);
|
||||
}
|
||||
if (filesystem_error) {
|
||||
std::filesystem::remove(temporary, filesystem_error);
|
||||
*error = "cannot publish atomic payload manifest";
|
||||
@@ -453,14 +468,19 @@ ManifestIdentity ReadManifest(const std::filesystem::path& path,
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(result.contents, nullptr, false, true);
|
||||
if (document.is_discarded() || !document.is_object() ||
|
||||
document.value("schema_version", 0) != 1 ||
|
||||
!document.contains("schema_version") ||
|
||||
!document["schema_version"].is_number_integer() ||
|
||||
document["schema_version"] != 1 ||
|
||||
!document.contains("payload_id") || !document["payload_id"].is_string()) {
|
||||
result.error = "active payload manifest is invalid";
|
||||
return result;
|
||||
}
|
||||
result.payload_id = document["payload_id"].get<std::string>();
|
||||
if (!ValidPayloadId(result.payload_id) ||
|
||||
document.value("payload_path", "") != "payloads/" + result.payload_id) {
|
||||
!document.contains("payload_path") ||
|
||||
!document["payload_path"].is_string() ||
|
||||
document["payload_path"].get_ref<const std::string&>() !=
|
||||
"payloads/" + result.payload_id) {
|
||||
result.error = "active payload manifest identity is invalid";
|
||||
}
|
||||
return result;
|
||||
@@ -535,7 +555,14 @@ bool PopulateInspectedResult(
|
||||
if (verified_payload != nullptr) *verified_payload = payload;
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(manifest.contents, nullptr, false, true);
|
||||
if (document.is_discarded() ||
|
||||
if (document.is_discarded() || !document.is_object() ||
|
||||
!document.contains("version_name") ||
|
||||
!document["version_name"].is_string() ||
|
||||
!document.contains("elf_build_id") ||
|
||||
!document["elf_build_id"].is_string() ||
|
||||
!document.contains("version_code") ||
|
||||
!document["version_code"].is_number_unsigned() ||
|
||||
document["version_code"] == 0 ||
|
||||
document.value("version_name", "") != payload.metadata.version_name ||
|
||||
document.value("version_code", 0ULL) != payload.metadata.version_code ||
|
||||
document.value("elf_build_id", "") != payload.metadata.build_id) {
|
||||
@@ -550,6 +577,13 @@ bool PopulateInspectedResult(
|
||||
result->error = "active payload manifest hash does not match payload";
|
||||
return false;
|
||||
}
|
||||
for (const char* field : {"host_abi_profile_path",
|
||||
"compatibility_manifest_path", "approval_path"}) {
|
||||
if (document.contains(field) && !document[field].is_string()) {
|
||||
result->error = "active payload approval reference set is invalid";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const std::string profile = document.value("host_abi_profile_path", "");
|
||||
const std::string compatibility =
|
||||
document.value("compatibility_manifest_path", "");
|
||||
@@ -591,14 +625,120 @@ bool PersistPayloadSha256(const std::filesystem::path& root,
|
||||
error);
|
||||
}
|
||||
|
||||
constexpr const char* kPreviousRecovery = ".previous_good-recovery.json";
|
||||
constexpr const char* kAbsentRecovery = ".previous_good-recovery.absent";
|
||||
|
||||
// Inspect the directory entry itself: a dangling symlink is also a pending
|
||||
// recovery artifact, and an inspection failure must never authorize deletion.
|
||||
bool NoPendingRecovery(const std::filesystem::path& root, std::string* error) {
|
||||
for (const char* name : {kPreviousRecovery, kAbsentRecovery}) {
|
||||
std::error_code filesystem_error;
|
||||
const auto status =
|
||||
std::filesystem::symlink_status(root / name, filesystem_error);
|
||||
if (filesystem_error &&
|
||||
filesystem_error != std::errc::no_such_file_or_directory) {
|
||||
*error = "cannot inspect previous_good recovery: " +
|
||||
filesystem_error.message();
|
||||
return false;
|
||||
}
|
||||
if (status.type() != std::filesystem::file_type::not_found) {
|
||||
*error = "previous_good recovery is pending: " + (root / name).string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveRecovery(const std::filesystem::path& recovery, std::string* error,
|
||||
bool current_published = false) {
|
||||
std::error_code filesystem_error;
|
||||
std::filesystem::remove(recovery, filesystem_error);
|
||||
if (!filesystem_error) return true;
|
||||
if (!error->empty()) *error += "; ";
|
||||
if (current_published) *error += "current manifest published; ";
|
||||
*error +=
|
||||
"cannot remove previous_good recovery: " + filesystem_error.message() +
|
||||
"; recovery retained at " + recovery.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called only while StoreLock is held. This recovers a failed publication;
|
||||
// it does not make two manifest renames atomic against a process crash.
|
||||
bool PublishActivation(const std::filesystem::path& root,
|
||||
std::string_view payload_id, std::string_view contents,
|
||||
const ManifestWriteFault& fault, std::string* error) {
|
||||
if (!NoPendingRecovery(root, error)) return false;
|
||||
const auto current = ReadManifest(root / "current.json", true);
|
||||
if (!current) {
|
||||
*error = current.error;
|
||||
return false;
|
||||
}
|
||||
if (current.contents.empty() || current.payload_id == payload_id) {
|
||||
return WriteAtomic(root, root / "current.json", contents, error, fault);
|
||||
}
|
||||
|
||||
std::error_code filesystem_error;
|
||||
const auto previous_status = std::filesystem::symlink_status(
|
||||
root / "previous_good.json", filesystem_error);
|
||||
if (filesystem_error &&
|
||||
filesystem_error != std::errc::no_such_file_or_directory) {
|
||||
*error =
|
||||
"cannot inspect previous_good manifest: " + filesystem_error.message();
|
||||
return false;
|
||||
}
|
||||
const bool previous_present =
|
||||
previous_status.type() != std::filesystem::file_type::not_found;
|
||||
std::string previous;
|
||||
if (previous_present) {
|
||||
// Keep the original bytes, including an explicitly present empty file.
|
||||
previous = ReadRegular(root / "previous_good.json", 1024U * 1024U, error);
|
||||
if (!error->empty()) return false;
|
||||
}
|
||||
const auto recovery =
|
||||
root / (previous_present ? kPreviousRecovery : kAbsentRecovery);
|
||||
if (!WriteAtomic(root, recovery, previous_present ? previous : "absent\n",
|
||||
error, fault)) {
|
||||
return false;
|
||||
}
|
||||
if (!WriteAtomic(root, root / "previous_good.json", current.contents, error,
|
||||
fault)) {
|
||||
RemoveRecovery(recovery, error);
|
||||
return false;
|
||||
}
|
||||
if (!WriteAtomic(root, root / "current.json", contents, error, fault)) {
|
||||
std::string restore_error;
|
||||
if (previous_present) {
|
||||
WriteAtomic(root, root / "previous_good.json", previous, &restore_error,
|
||||
fault);
|
||||
} else {
|
||||
filesystem_error.clear();
|
||||
std::filesystem::remove(root / "previous_good.json", filesystem_error);
|
||||
if (filesystem_error) {
|
||||
restore_error = "cannot restore absence of previous_good: " +
|
||||
filesystem_error.message();
|
||||
}
|
||||
}
|
||||
if (!restore_error.empty()) {
|
||||
*error += "; previous_good restoration failed: " + restore_error +
|
||||
"; recovery retained at " + recovery.string();
|
||||
} else {
|
||||
RemoveRecovery(recovery, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return RemoveRecovery(recovery, error, true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PayloadStore::PayloadStore(std::filesystem::path root,
|
||||
std::filesystem::path compatibility_manifest,
|
||||
std::filesystem::path runtime_binary)
|
||||
std::filesystem::path runtime_binary,
|
||||
ManifestWriteFault manifest_write_fault)
|
||||
: root_(std::move(root)),
|
||||
compatibility_manifest_(std::move(compatibility_manifest)),
|
||||
runtime_binary_(std::move(runtime_binary)) {}
|
||||
runtime_binary_(std::move(runtime_binary)),
|
||||
manifest_write_fault_(std::move(manifest_write_fault)) {}
|
||||
|
||||
PayloadStoreResult PayloadStore::Stage(
|
||||
const std::filesystem::path& prepared_payload) {
|
||||
@@ -679,20 +819,9 @@ PayloadStoreResult PayloadStore::Promote(std::string_view payload_id) {
|
||||
result.version_name = payload.metadata.version_name;
|
||||
result.version_code = payload.metadata.version_code;
|
||||
result.build_id = payload.metadata.build_id;
|
||||
const ManifestIdentity current = ReadManifest(root_ / "current.json", true);
|
||||
if (!current && !current.error.empty()) {
|
||||
result.error = current.error;
|
||||
return result;
|
||||
}
|
||||
if (!current.contents.empty() && current.payload_id != result.payload_id &&
|
||||
!WriteAtomic(root_, root_ / "previous_good.json", current.contents,
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
const std::string contents = ActivationManifest(payload).dump(2) + "\n";
|
||||
if (!WriteAtomic(root_, root_ / "current.json", contents, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
PublishActivation(root_, result.payload_id, contents, manifest_write_fault_,
|
||||
&result.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -742,17 +871,8 @@ PayloadStoreResult PayloadStore::PromoteProbation(
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
const ManifestIdentity current = ReadManifest(root_ / "current.json", true);
|
||||
if (!current && !current.error.empty()) {
|
||||
result.error = current.error;
|
||||
return result;
|
||||
}
|
||||
if (!current.contents.empty() && current.payload_id != result.payload_id &&
|
||||
!WriteAtomic(root_, root_ / "previous_good.json", current.contents,
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
if (!WriteAtomic(root_, root_ / "current.json", contents, &result.error)) {
|
||||
if (!PublishActivation(root_, result.payload_id, contents,
|
||||
manifest_write_fault_, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
result.version_name = payload.metadata.version_name;
|
||||
@@ -875,6 +995,7 @@ PayloadGarbageResult PayloadStore::CollectGarbage(
|
||||
PayloadGarbageResult result;
|
||||
StoreLock lock(root_, &result.error);
|
||||
if (!lock) return result;
|
||||
if (!NoPendingRecovery(root_, &result.error)) return result;
|
||||
std::set<std::string> retained(keep.begin(), keep.end());
|
||||
for (const char* manifest : {"current.json", "previous_good.json"}) {
|
||||
const ManifestIdentity identity = ReadManifest(root_ / manifest, true);
|
||||
|
||||
@@ -302,6 +302,13 @@ CanaryResult RunReadinessCanary(const CanaryOptions& options) {
|
||||
"DISPLAY",
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"XDG_SESSION_TYPE",
|
||||
"SDL_VIDEO_DRIVER",
|
||||
"SDL_VIDEODRIVER",
|
||||
"MOCKTAIL_FORCE_WAYLAND",
|
||||
"MOCKTAIL_FORCE_X11",
|
||||
"MOCKTAIL_ANGLE_FORCE_X11",
|
||||
"MOCKTAIL_PREFER_WAYLAND",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"PULSE_SERVER",
|
||||
"PIPEWIRE_REMOTE",
|
||||
@@ -419,8 +426,13 @@ CanaryResult RunReadinessCanary(const CanaryOptions& options) {
|
||||
}
|
||||
pid_t child = -1;
|
||||
if (spawn_status == 0) {
|
||||
spawn_status = posix_spawn(&child, options.runtime_binary.c_str(), &actions,
|
||||
nullptr, arguments, environment_pointers.data());
|
||||
result.spawn_error =
|
||||
options.spawn
|
||||
? options.spawn(&child, options.runtime_binary.c_str(), &actions,
|
||||
nullptr, arguments, environment_pointers.data())
|
||||
: posix_spawn(&child, options.runtime_binary.c_str(), &actions,
|
||||
nullptr, arguments, environment_pointers.data());
|
||||
spawn_status = result.spawn_error;
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(log_descriptor);
|
||||
|
||||
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user