Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bde1a1c0d | ||
|
|
60e9c5c3ef | ||
|
|
d76e1b6f54 | ||
|
|
33c06c063e | ||
|
|
9e8afb233a | ||
|
|
7fda1eb239 | ||
|
|
f128e28b83 | ||
|
|
718e67f9e1 | ||
|
|
c6c354fdb1 | ||
|
|
25930d14ff | ||
|
|
65030d8df1 | ||
|
|
0be5e808c5 | ||
|
|
1b030a57eb | ||
|
|
2f5527b8b5 | ||
|
|
579cedbf1d | ||
|
|
a7b90e5919 | ||
|
|
3bba795ea3 | ||
|
|
5ce4487d4f | ||
|
|
c544389dd0 | ||
|
|
bdb84142d7 | ||
|
|
40698efb7d | ||
|
|
3d2a2b3d98 | ||
|
|
78a314b761 | ||
|
|
2d1dd3788b | ||
|
|
56d3160641 | ||
|
|
5db99041bc | ||
|
|
9ec56338c3 | ||
|
|
894ebf8eb7 | ||
|
|
6bec8a6c5c | ||
|
|
b9e7bc761b | ||
|
|
f87aaca2fb | ||
|
|
901ba615ab | ||
|
|
f098bc1b5d | ||
|
|
e1f7c8c06c | ||
|
|
a9987546d5 | ||
|
|
6a39887e8f | ||
|
|
349e314850 | ||
|
|
b1112d8cdc | ||
|
|
27c383a5d9 | ||
|
|
7cf24feef6 | ||
|
|
4443766d39 | ||
|
|
ea2166b09a | ||
|
|
9e4e69be1c | ||
|
|
dd0655bfde | ||
|
|
d2d275706b | ||
|
|
b7b63dd30f | ||
|
|
aacab4036c | ||
|
|
e104b1d936 | ||
|
|
bf6a6b417d | ||
|
|
dfd35b8cc8 | ||
|
|
1632681e6c | ||
|
|
88d10d53f2 | ||
|
|
eae9fa7030 | ||
|
|
ad76f68224 | ||
|
|
d783793b4e |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Bug report
|
||||
description: Report a reproducible problem in Mocktail
|
||||
description: Report a reproducible problem in Nightcap
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
@@ -75,7 +76,7 @@ body:
|
||||
value: |
|
||||
Latest log:
|
||||
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
- Other installs: `~/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
Older logs are in the `sessions` directory next to `latest.log`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Flatpak
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Native packages]
|
||||
types: [completed]
|
||||
push:
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
@@ -15,13 +15,11 @@ on:
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'packaging/discord-join.html'
|
||||
- 'packaging/icons/**'
|
||||
- 'packaging/space.bigrat.mocktail.desktop'
|
||||
- 'packaging/space.bigrat.mocktail.metainfo.xml'
|
||||
- 'packaging/space.bigrat.mocktail.svg'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.desktop'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.metainfo.xml'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/assemble_native_repositories.sh'
|
||||
- 'site/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
@@ -29,61 +27,28 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: flatpak-${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: flatpak-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
if: >-
|
||||
github.event_name != 'workflow_run' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main')
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
MOCKTAIL_PUBLISH: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == 'main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
MOCKTAIL_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
# Publish on release tags and on manual runs from main. Pull requests
|
||||
# only verify that the manifest still builds.
|
||||
MOCKTAIL_PUBLISH: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Download package repositories
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
run_id="${{ github.event.workflow_run.id }}"
|
||||
if [[ -z "$run_id" ]]; then
|
||||
run_id="$(gh run list \
|
||||
--workflow packages.yml \
|
||||
--branch main \
|
||||
--event push \
|
||||
--status success \
|
||||
--limit 1 \
|
||||
--json databaseId \
|
||||
--jq '.[0].databaseId')"
|
||||
fi
|
||||
[[ -n "$run_id" ]]
|
||||
gh run download "$run_id" \
|
||||
--name Mocktail-native-repositories \
|
||||
--dir native-repositories
|
||||
[[ -d native-repositories/apt && \
|
||||
-d native-repositories/rpm && \
|
||||
-d native-repositories/downloads && \
|
||||
-s native-repositories/mocktail-packages.gpg ]]
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -99,22 +64,23 @@ jobs:
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
|
||||
# The manifest builds the local checkout from refs/heads/main.
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
install -d -m 0700 "$RUNNER_TEMP/nightcap-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >nightcap-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
"--gpg-homedir=$RUNNER_TEMP/nightcap-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
bundle_keys=(--gpg-keys=nightcap-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
@@ -127,34 +93,33 @@ jobs:
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--gpg-homedir="$RUNNER_TEMP/nightcap-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
Nightcap-x86_64.flatpak \
|
||||
io.github.CoderDayton.nightcap \
|
||||
stable
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
|
||||
public native-repositories
|
||||
repo Nightcap-x86_64.flatpak nightcap-flatpak.gpg public
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-x86_64-flatpak
|
||||
path: Mocktail-x86_64.flatpak
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: Nightcap-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
@@ -170,13 +135,7 @@ jobs:
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: >-
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main') ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main')
|
||||
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
@@ -191,3 +150,43 @@ jobs:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
release:
|
||||
name: Attach bundle to GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download Flatpak
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: release
|
||||
|
||||
- name: Upload to release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
(cd release && sha256sum Nightcap-x86_64.flatpak > Nightcap-x86_64.flatpak.sha256)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# The Release workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.flatpak \
|
||||
release/Nightcap-x86_64.flatpak.sha256
|
||||
@@ -1,39 +1,47 @@
|
||||
name: Native packages
|
||||
name: Nightly AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
schedule:
|
||||
# 05:00 UTC daily. A run whose commit already carries the continuous tag
|
||||
# exits without rebuilding, so a quiet day costs one probe job.
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: packages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: nightly-appimage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
probe:
|
||||
name: Check for new commits
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
build: ${{ steps.decide.outputs.build }}
|
||||
steps:
|
||||
- name: Compare HEAD against the published continuous tag
|
||||
id: decide
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
published="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/continuous" \
|
||||
--jq .object.sha 2>/dev/null || true)"
|
||||
if [[ "$GITHUB_EVENT_NAME" == "schedule" && "$published" == "$GITHUB_SHA" ]]; then
|
||||
echo 'build=false' >>"$GITHUB_OUTPUT"
|
||||
echo "continuous already points at $GITHUB_SHA; nothing to build"
|
||||
else
|
||||
echo 'build=true' >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
needs: probe
|
||||
if: needs.probe.outputs.build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
@@ -138,155 +146,17 @@ jobs:
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
name: Nightcap-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
build-essential cmake git ninja-build pkg-config lld fakeroot \
|
||||
dpkg-dev file libsdl3-dev libsdl3-ttf-dev \
|
||||
libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \
|
||||
libyaml-dev libelf-dev libminizip-dev libcapstone-dev \
|
||||
libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
|
||||
libutf8proc-dev libfontconfig1-dev libegl-dev \
|
||||
libvulkan-dev libplacebo-dev zlib1g-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
-G DEB -B dist/deb
|
||||
dpkg-deb --info dist/deb/*.deb >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-DEB
|
||||
path: dist/deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
rpm:
|
||||
name: RPM (Fedora 44)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config lld rpm-build \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \
|
||||
webkitgtk6.0-devel utf8proc-devel fontconfig-devel \
|
||||
libglvnd-devel vulkan-headers vulkan-loader-devel \
|
||||
libplacebo-devel zlib-ng-compat-devel
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
-G RPM -B dist/rpm
|
||||
rpm --query --package --info dist/rpm/*.rpm >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-RPM
|
||||
path: dist/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
arch:
|
||||
name: Pacman package (Arch Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build pacman package
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
pacman -Syu --needed --noconfirm \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig \
|
||||
libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
makepkg --dir /workspace/packaging/arch \
|
||||
--noconfirm --cleanbuild --force
|
||||
install -d dist/arch
|
||||
cp packaging/arch/*.pkg.tar.zst dist/arch/
|
||||
pacman -Qp dist/arch/*.pkg.tar.zst >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload pacman package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-Arch
|
||||
path: dist/arch/*.pkg.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish continuous packages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
name: Publish continuous release
|
||||
needs: [probe, appimage]
|
||||
# A manual run from another branch still builds and smoke-tests, but the
|
||||
# continuous tag and release only ever follow main.
|
||||
if: needs.probe.outputs.build == 'true' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
@@ -297,84 +167,49 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download packages
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Mocktail-*
|
||||
name: Nightcap-AppImage
|
||||
path: release
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare continuous packages
|
||||
- name: Name and checksum the nightly build
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
mv -- release/*.AppImage release/mocktail-nightly.AppImage
|
||||
mv -- release/*.deb release/mocktail-nightly.deb
|
||||
mv -- release/*.rpm release/mocktail-nightly.rpm
|
||||
mv -- release/*.pkg.tar.zst release/mocktail-nightly.pkg.tar.zst
|
||||
# The Makefile builds under upstream's artifact name; the published
|
||||
# asset carries this fork's.
|
||||
mv -- release/Mocktail-x86_64.AppImage release/Nightcap-nightly-x86_64.AppImage
|
||||
(cd release && sha256sum Nightcap-nightly-x86_64.AppImage \
|
||||
> Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
find release -maxdepth 1 -type f -print
|
||||
|
||||
- name: Build package repositories
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && -n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
apt-utils createrepo-c gnupg rpm
|
||||
install -d stable/deb stable/rpm
|
||||
stable_tag="$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name)"
|
||||
gh release download "$stable_tag" --pattern '*.deb' --dir stable/deb
|
||||
gh release download "$stable_tag" --pattern '*.rpm' --dir stable/rpm
|
||||
mapfile -t stable_debs < <(find stable/deb -maxdepth 1 -type f -name '*.deb' -print)
|
||||
mapfile -t stable_rpms < <(find stable/rpm -maxdepth 1 -type f -name '*.rpm' -print)
|
||||
[[ "${#stable_debs[@]}" -eq 1 && "${#stable_rpms[@]}" -eq 1 ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-native-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-packages.gpg
|
||||
./scripts/assemble_native_repositories.sh \
|
||||
"${stable_debs[0]}" release/mocktail-nightly.deb \
|
||||
"${stable_rpms[0]}" release/mocktail-nightly.rpm \
|
||||
mocktail-packages.gpg "$FLATPAK_GPG_KEY_ID" \
|
||||
"$RUNNER_TEMP/mocktail-native-gnupg" native-repositories
|
||||
|
||||
- name: Update continuous release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git config user.name komaruworld
|
||||
git config user.email rtysus228@gmail.com
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git tag --force continuous "$GITHUB_SHA"
|
||||
git push origin refs/tags/continuous --force
|
||||
notes="Automated build of main at ${GITHUB_SHA:0:8}. Untested and unreleased; use a tagged release for normal play."
|
||||
if ! gh release view continuous >/dev/null 2>&1; then
|
||||
gh release create continuous \
|
||||
--prerelease \
|
||||
--title "Mocktail continuous" \
|
||||
--notes "Automated development packages from main."
|
||||
--title "Nightcap continuous" \
|
||||
--notes "$notes"
|
||||
fi
|
||||
gh release edit continuous --title "Nightcap continuous" --notes "$notes"
|
||||
gh release upload continuous release/* --clobber
|
||||
# Drop assets left by earlier runs under other names.
|
||||
while IFS= read -r asset; do
|
||||
case "$asset" in
|
||||
mocktail-nightly.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
|
||||
Nightcap-nightly-x86_64.AppImage|Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
;;
|
||||
Mocktail-*.AppImage|mocktail_*.deb|mocktail-*.deb|mocktail-*.rpm|mocktail-*.pkg.tar.zst)
|
||||
*)
|
||||
gh release delete-asset continuous "$asset" --yes
|
||||
;;
|
||||
esac
|
||||
done < <(gh release view continuous --json assets --jq '.assets[].name')
|
||||
|
||||
- name: Upload package repositories
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-native-repositories
|
||||
path: native-repositories
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,168 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check release notes
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: ./scripts/release_notes.sh "$GITHUB_REF_NAME" >/dev/null
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-x86_64
|
||||
echo "a4d51752f9861b4870ca529fea04087310b28f8943cc55b99a95b777d4db92ec /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-x86_64-linux
|
||||
echo "0d01a4e2628efc897be9663f0faeed6a171e1851270b4e1ce4c22434a36ecebd /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-x86_64
|
||||
echo "cef962c299f2fa19b2b3cdf2fa1565ee8541796cc89b9a97a591f94041e8b083 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-x86_64
|
||||
echo "50891c38ba359db8271819a6cbf6aaa8068681523f0c4f2b8242007a45edaa28 /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-x86_64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [appimage]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: release
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
mv release/Mocktail-x86_64.AppImage release/Nightcap-x86_64.AppImage
|
||||
(cd release && sha256sum Nightcap-x86_64.AppImage > Nightcap-x86_64.AppImage.sha256)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# The Flatpak workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.AppImage \
|
||||
release/Nightcap-x86_64.AppImage.sha256
|
||||
@@ -1,5 +1,6 @@
|
||||
# Build
|
||||
/build/
|
||||
/build-test/
|
||||
/build_*/
|
||||
/build-flatpak/
|
||||
/build-anylinux*/
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Nightcap. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Release notes on
|
||||
GitHub are taken from the matching section here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
|
||||
- Custom Discord presence. `integrations.discord_rpc.text.title` replaces the
|
||||
name in "Playing Nightcap", and `integrations.discord_rpc.images` sets the
|
||||
large and small icons and their hover text. Text and image fields accept
|
||||
`{place_name}`, `{place_icon}`, and `{creator_name}`. A field that expands
|
||||
an empty placeholder is left out, and `text.state: ""` hides the state line.
|
||||
- The default Discord card now matches Sober: the title is "Roblox", the
|
||||
details line is "Playing <experience>", the state is "by <creator>", and
|
||||
the Roblox badge sits inside the experience icon. The creator comes from the
|
||||
same Roblox games request that already fetches the experience name. With
|
||||
`show_place_name: false` the creator is hidden too, so the card shows only
|
||||
the title and timer.
|
||||
- `--profile <file>` writes a Chrome-format trace of the Vulkan adapter's
|
||||
work: presents, GPU waits, submits, ETC2 texture decode, and pipeline and
|
||||
shader creation. Open it in ui.perfetto.dev. `docs/BENCHMARKING.md`
|
||||
describes the benchmark runs, and `scripts/summarize_profile_trace.py`
|
||||
turns traces into a before and after table.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Texture uploads stalled the frame they were submitted on. ETC2 batches
|
||||
decoded on worker threads but were then resampled one upload at a time on
|
||||
the submitting thread, which at the default 4x upscale is sixteen times the
|
||||
decoded texels. Resampling now runs on the same worker pool, in two passes so
|
||||
mip level 0 still resolves before the mips built from it. In a 70k-part
|
||||
place the worst `vkQueueSubmit` fell from 167ms to 92ms.
|
||||
- Most ETC2 batches decoded on a single thread. Workers were created and
|
||||
joined per batch, so the parallel floor had to cover that cost and sat at a
|
||||
1024x1024 image; batches under it ran on the caller, which was 54% of all
|
||||
decode time in a 70k-part place. Decode workers are now a persistent pool,
|
||||
and the floor drops to a 128x128 image. Mean decode time in that place fell
|
||||
from 8.2ms to 4.7ms, and 11.8ms before both texture fixes.
|
||||
- On CPUs with a wide core-speed spread, Feral GameMode pinned the process to
|
||||
the few fastest cores, concentrating Roblox's worker pool onto them instead
|
||||
of spreading it. With `performance.gamemode: auto`, a request that narrows
|
||||
the CPU affinity is now released again, which releases the pin. Set
|
||||
`pin_cores=no` in `~/.config/gamemode.ini` to keep GameMode without the
|
||||
pinning, or `performance.gamemode: on` to accept it. See
|
||||
`docs/PERFORMANCE.md`.
|
||||
- With `show_place_name: false`, Discord no longer shows the experience
|
||||
thumbnail or its name on hover.
|
||||
- The host main thread no longer spins on the engine's message pump, which
|
||||
cost about half a core and 146,000 empty calls a second. It now rests 100 µs
|
||||
between polls: sleeping when the governor is `performance`, parking the
|
||||
core with TPAUSE on CPUs that have it, spinning otherwise. With GameMode
|
||||
setting the governor, process CPU in a 60k-part place fell from 216% to
|
||||
107% at the same 120 fps. `MOCKTAIL_ENGINE_PUMP_REST` forces a mode; see
|
||||
`docs/PERFORMANCE.md`.
|
||||
- ETC2 uploads with a `bufferRowLength` or `bufferImageHeight` larger than
|
||||
the image read the padded rows as packed data and decoded garbage.
|
||||
- ETC2 resampling created and joined worker threads on every batch. It now
|
||||
runs on the persistent decode pool.
|
||||
- A failed trace write left the profile writer thread waking every 250 ms
|
||||
for the rest of the session. It now exits.
|
||||
- The `--profile` trace gains a `pump` slice for the engine's main-thread
|
||||
step.
|
||||
|
||||
## [0.2.0] - 2026-09-16
|
||||
|
||||
### Added
|
||||
|
||||
- FAQ covering Discord RPC, log locations, and VR, adapted from upstream.
|
||||
- `input.raw_mouse` in `config.yaml` sends mouse look deltas in the host's
|
||||
own units. Turn it on when first-person aiming feels too slow or too fast
|
||||
under fractional display scaling. Off by default.
|
||||
- Nightly AppImage builds, published as the `continuous` prerelease. They are
|
||||
untested; use a tagged release for normal play.
|
||||
|
||||
### Changed
|
||||
|
||||
- Values in `fflags.json` now win over the performance preset instead of
|
||||
stopping the launch. Startup prints each key it kept.
|
||||
- Discord Rich Presence uses Nightcap's own Discord application, so it shows
|
||||
as "Playing Nightcap". Terms and privacy pages for it live on the
|
||||
project site.
|
||||
- Discord Rich Presence is on by default. Set
|
||||
`integrations.discord_rpc.enabled: false` to turn it off.
|
||||
- The AppImage build verifies its reduced dependency packages against digests
|
||||
taken at build time, rather than a digest list kept in the repository.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mouse look keeps responding after a long turn. The tracked pointer position
|
||||
could drift past the edge of the view, which swallowed every reversal until
|
||||
the mouse travelled all the way back.
|
||||
- Nightcap no longer refuses to start with `cannot lock appStorage`. It
|
||||
created its own credential directory using the process umask, then rejected
|
||||
that directory for being group writable. Systems with a permissive umask,
|
||||
such as `002`, failed on first run.
|
||||
|
||||
## [0.1.0] - 2026-09-15
|
||||
|
||||
First Nightcap release, forked from komaruworld/mocktail.
|
||||
|
||||
### Added
|
||||
|
||||
- ETC2 and EAC texture emulation on Vulkan, so games that use compressed
|
||||
mobile textures render correctly on desktop GPUs. Decoding runs on worker
|
||||
threads to avoid stutter.
|
||||
- PNG texture overrides in `~/.config/mocktail/textures`, keyed by upload
|
||||
hash, with a dump mode to find the texture you want to replace.
|
||||
- Small textures are upscaled 4x by default. Set
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn it off.
|
||||
- AppImage and signed Flatpak builds on every release tag. The Flatpak repo
|
||||
is published at https://coderdayton.github.io/nightcap/.
|
||||
- Roadmap in `docs/ROADMAP.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Frames are presented as soon as they are ready instead of waiting in a
|
||||
queue, which lowers input latency.
|
||||
- Roblox now sees the real host RAM and display size instead of a low-end
|
||||
phone profile.
|
||||
- App ID is `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream Mocktail. The binary and config paths keep the `mocktail` name.
|
||||
- New name, icon, and colors.
|
||||
- The desktop entry launches on Wayland and points at the installed binary.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Servers no longer kick with error 319 shortly after joining.
|
||||
- The FMOD output-device bridge is optional, so the updater no longer fails
|
||||
without it.
|
||||
- The AppImage build no longer fails on stale checksums for the slimmed
|
||||
Arch packages.
|
||||
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.0...HEAD
|
||||
[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
|
||||
[0.1.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.1.0
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -60,7 +61,7 @@ set(MOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST
|
||||
CACHE FILEPATH
|
||||
"Fallback signing trust manifest used outside an installed runtime"
|
||||
)
|
||||
set(MOCKTAIL_DISCORD_APPLICATION_ID "1537088975720812655" CACHE STRING
|
||||
set(MOCKTAIL_DISCORD_APPLICATION_ID "1549428231927500843" CACHE STRING
|
||||
"Discord application ID baked into Mocktail Rich Presence releases"
|
||||
)
|
||||
string(LENGTH "${MOCKTAIL_DISCORD_APPLICATION_ID}"
|
||||
@@ -265,6 +266,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/crash_report_policy.cc
|
||||
src/runtime/discord_rpc.cc
|
||||
src/runtime/device_profile.cc
|
||||
src/runtime/engine_pump_rest.cc
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
@@ -300,6 +302,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/support_bundle.cc
|
||||
src/runtime/system_proxy.cc
|
||||
src/runtime/texture_memory_policy.cc
|
||||
src/runtime/device_memory_profile.cc
|
||||
)
|
||||
add_library(Mocktail::Runtime ALIAS mocktail_runtime)
|
||||
target_include_directories(mocktail_runtime PUBLIC include)
|
||||
@@ -724,13 +727,24 @@ install(FILES
|
||||
config/roblox_signing_certificates.json
|
||||
DESTINATION "${MOCKTAIL_INSTALL_DATA_DIR}/metadata"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.desktop
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.metainfo.xml
|
||||
# The installed desktop entry names the binary by its install path, so it
|
||||
# launches from sessions whose PATH lacks the install prefix. Uses the prefix
|
||||
# given at install time, not the configure-time one.
|
||||
install(CODE "
|
||||
file(READ \"${CMAKE_SOURCE_DIR}/packaging/io.github.CoderDayton.nightcap.desktop\"
|
||||
MOCKTAIL_DESKTOP_ENTRY)
|
||||
string(REPLACE \"Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u\"
|
||||
\"Exec=env SDL_VIDEODRIVER=wayland,x11 \${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/mocktail %u\"
|
||||
MOCKTAIL_DESKTOP_ENTRY \"\${MOCKTAIL_DESKTOP_ENTRY}\")
|
||||
file(WRITE \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
|
||||
\"\${MOCKTAIL_DESKTOP_ENTRY}\")
|
||||
file(INSTALL \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
|
||||
DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications\")
|
||||
")
|
||||
install(FILES packaging/io.github.CoderDayton.nightcap.metainfo.xml
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.svg
|
||||
install(FILES packaging/io.github.CoderDayton.nightcap.svg
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
|
||||
)
|
||||
set(MOCKTAIL_HICOLOR_ICON_SIZES
|
||||
@@ -739,7 +753,7 @@ set(MOCKTAIL_HICOLOR_ICON_SIZES
|
||||
# Generated icons are committed, so ImageMagick is not a build dependency.
|
||||
foreach(icon_size IN LISTS MOCKTAIL_HICOLOR_ICON_SIZES)
|
||||
install(FILES
|
||||
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/space.bigrat.mocktail.png"
|
||||
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/io.github.CoderDayton.nightcap.png"
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_DATADIR}/icons/hicolor/${icon_size}x${icon_size}/apps"
|
||||
)
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
# FAQ
|
||||
|
||||
## How do I enable Discord RPC?
|
||||
## How do I turn Discord RPC off?
|
||||
|
||||
RPC is disabled by default. Launch Mocktail once to create the config, then
|
||||
close it. Open `config.yaml`:
|
||||
RPC is on by default. It never signs in to Discord and never reads an
|
||||
account token. To turn it off, launch Nightcap once to create the config,
|
||||
then close it. Open `config.yaml`:
|
||||
|
||||
- Native / AppImage: `~/.config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/config/mocktail/config.yaml`
|
||||
|
||||
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
|
||||
|
||||
Set `enabled` to `true` under `integrations.discord_rpc`. If the block is
|
||||
Set `enabled` to `false` under `integrations.discord_rpc`. If the block is
|
||||
missing, add it under the existing `integrations` section:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
discord_rpc:
|
||||
enabled: true
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Save the file, open Discord Desktop, and restart Mocktail. Set `enabled`
|
||||
back to `false` to disable RPC.
|
||||
Save the file and restart Nightcap. Set `enabled` back to `true` to turn
|
||||
RPC on again.
|
||||
|
||||
## Can Discord count Nightcap as Roblox play time or quests?
|
||||
|
||||
No. Discord only credits play time and quests to games its own scanner
|
||||
detects, and Rich Presence never counts. On Linux the scanner only matches
|
||||
games with a Linux entry in Discord's list. Roblox has none, so no process
|
||||
name Nightcap could use would be detected. Only Roblox or Discord can change
|
||||
that.
|
||||
|
||||
## Where can I find logs for a bug report?
|
||||
|
||||
Mocktail saves logs automatically. Reproduce the issue, close Mocktail, and
|
||||
Nightcap saves logs automatically. Reproduce the issue, close Nightcap, and
|
||||
attach `latest.log`:
|
||||
|
||||
- Native / AppImage: `~/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
|
||||
|
||||
## Can I play in VR?
|
||||
|
||||
VR is experimental. Install the [build dependencies](README.md#building),
|
||||
then switch to the `vr` branch and build:
|
||||
VR is experimental and lives on upstream's `vr` branch, which does not
|
||||
include the Nightcap changes. Install the
|
||||
[build dependencies](README.md#building), then switch to that branch and
|
||||
build:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git switch vr
|
||||
git remote add upstream https://github.com/komaruworld/mocktail.git
|
||||
git fetch upstream
|
||||
git switch -c vr upstream/vr
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch payload-status payload-rollback support-bundle clean submodules help
|
||||
|
||||
@@ -19,7 +20,7 @@ ANYLINUX_PACKAGER ?= quick-sharun
|
||||
ANYLINUX_APPIMAGETOOL ?= appimagetool
|
||||
PREFIX ?= /usr
|
||||
FLATPAK_BUILD_DIR ?= build-flatpak
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/space.bigrat.mocktail.json
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
FLATPAK_JOBS ?= 4
|
||||
|
||||
define build_native_runtime
|
||||
@@ -55,7 +56,7 @@ install: ## Install an already-built dynamic runtime (use: sudo make install)
|
||||
register-url-handler: build ## Select this build for Roblox website links
|
||||
@./scripts/register_url_handler.sh --set-default \
|
||||
--executable "$(abspath $(BINARY))" \
|
||||
--desktop-file "$(abspath packaging/space.bigrat.mocktail.desktop)"
|
||||
--desktop-file "$(abspath packaging/io.github.CoderDayton.nightcap.desktop)"
|
||||
|
||||
test: ## Build and run all unit tests
|
||||
$(call build_native_runtime,Debug,ON)
|
||||
@@ -91,7 +92,7 @@ flatpak: ## Build and install the local x86-64 Flatpak
|
||||
--jobs "$(FLATPAK_JOBS)"
|
||||
|
||||
run-flatpak: ## Run the installed Flatpak
|
||||
@flatpak run space.bigrat.mocktail
|
||||
@flatpak run io.github.CoderDayton.nightcap
|
||||
|
||||
run: export MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS = 0
|
||||
run: build ## Run the native binary interactively until the window is closed
|
||||
|
||||
@@ -1,19 +1,71 @@
|
||||
# Mocktail
|
||||
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
|
||||
# Nightcap
|
||||
|
||||
[](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
|
||||
[](https://github.com/komaruworld/mocktail/stargazers)
|
||||
[](https://github.com/komaruworld/mocktail/releases/latest)
|
||||
[](https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml)
|
||||
[](https://github.com/CoderDayton/nightcap/stargazers)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/rhgpfcmFSD)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
|
||||
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
|
||||
the Linux side.
|
||||
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
|
||||
FreeBSD's Linuxulator. It provides the Android ABI and JNI pieces the client
|
||||
expects, then connects them to SDL3 and Vulkan or OpenGL on the Linux side.
|
||||
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Nightcap is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
|
||||
## About this fork
|
||||
|
||||
Nightcap is a fork of [komaruworld/mocktail](https://github.com/komaruworld/mocktail)
|
||||
with extra fixes for playing on a PC. The program and config paths keep the
|
||||
`mocktail` name so upstream fixes still apply cleanly. The app ID is
|
||||
`io.github.CoderDayton.nightcap`, so Nightcap and upstream can be installed
|
||||
side by side.
|
||||
|
||||
- **Lower input lag.** Frames are shown as soon as they are ready instead of
|
||||
waiting in a queue.
|
||||
- **Sharper textures.** Small textures are upscaled 4x by default, and you
|
||||
can replace any texture with your own PNG in `~/.config/mocktail/textures`.
|
||||
A ready-made crosshair is in [`textures/`](textures/README.md). Set
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn the upscale off.
|
||||
- **ETC2 textures on any GPU.** Games that use compressed mobile textures now
|
||||
render correctly on desktop GPUs, without stutter.
|
||||
- **No more error 319 kicks.** Servers no longer drop you shortly after
|
||||
joining.
|
||||
- **Correct device info.** Roblox sees your real RAM and screen size.
|
||||
- **Better desktop launcher.** Runs on Wayland and always finds the installed
|
||||
binary.
|
||||
|
||||
Everything else works the same as upstream. See the
|
||||
[roadmap](docs/ROADMAP.md) for what is coming next, the
|
||||
[changelog](CHANGELOG.md) for what each release changed,
|
||||
[performance](docs/PERFORMANCE.md) for CPU load, heat and the knobs that change
|
||||
them, and the [FAQ](FAQ.md) for Discord RPC, logs, and VR.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap
|
||||
```
|
||||
|
||||
Updates arrive through `flatpak update`. Nightcap is not on Flathub yet.
|
||||
|
||||
## AppImage
|
||||
|
||||
Grab the AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest),
|
||||
make it executable, and run it:
|
||||
|
||||
```bash
|
||||
chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Or build and install it from source using the steps under
|
||||
[Building](#building). There are no AUR, DEB, or RPM packages of Nightcap.
|
||||
Upstream ships those for unmodified Mocktail, see
|
||||
[komaruworld/mocktail](https://github.com/komaruworld/mocktail#readme).
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
@@ -21,91 +73,24 @@ Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
|
||||
```
|
||||
|
||||
The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Mocktail. The last working copy is kept in case
|
||||
launch and is not bundled with Nightcap. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
Install the stable release from Flathub:
|
||||
|
||||
```bash
|
||||
flatpak install flathub space.bigrat.mocktail
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
Nightly builds are produced automatically from the latest `main` branch:
|
||||
|
||||
```bash
|
||||
flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
## Install from the AUR
|
||||
|
||||
Arch Linux users can install either the pinned source release (`mocktail`) or
|
||||
the current development version (`mocktail-git`) with an AUR helper:
|
||||
|
||||
```bash
|
||||
paru -S mocktail-git
|
||||
# or
|
||||
yay -S mocktail-git
|
||||
```
|
||||
|
||||
Use `mocktail` instead of `mocktail-git` to build the pinned release from
|
||||
source, or install the stable prebuilt package with `paru -S mocktail-bin` or
|
||||
`yay -S mocktail-bin`.
|
||||
|
||||
## Install with DNF
|
||||
|
||||
Fedora 44 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo \
|
||||
-o /etc/yum.repos.d/mocktail.repo
|
||||
sudo dnf install mocktail
|
||||
# or
|
||||
sudo dnf install mocktail-nightly
|
||||
```
|
||||
|
||||
## Install with APT
|
||||
|
||||
Ubuntu 26.04 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg \
|
||||
-o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | \
|
||||
sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install mocktail
|
||||
# or
|
||||
sudo apt install mocktail-nightly
|
||||
```
|
||||
|
||||
## Direct downloads
|
||||
|
||||
Direct AppImage, DEB, and RPM downloads are available from the
|
||||
[Website](https://mocktail.bigrat.space/) and
|
||||
[GitHub Releases](https://github.com/komaruworld/mocktail/releases).
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## FFlag overrides
|
||||
|
||||
Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
`~/.config/mocktail/fflags.json`). Mocktail applies it on the next launch.
|
||||
`~/.config/mocktail/fflags.json`). Nightcap applies it on the next launch.
|
||||
|
||||
PC profiles use the legacy Charts page by default to avoid the blank screen
|
||||
caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
@@ -117,15 +102,18 @@ caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
"DFIntExample": "120"
|
||||
}
|
||||
```
|
||||
## FAQ
|
||||
## Settings
|
||||
|
||||
To see the most Frequently Asked Question go check [FAQ.md](FAQ.md)
|
||||
Edit `~/.config/mocktail/config.yaml`. Useful options:
|
||||
|
||||
- `graphics.frame_rate_limit: display` to unlock FPS to your monitor's rate.
|
||||
- `graphics.vsync: off` for the lowest lag, at the cost of tearing.
|
||||
- `graphics.backend: opengl` if Vulkan does not work on your GPU.
|
||||
|
||||
## Building
|
||||
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Nightcap runs
|
||||
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
|
||||
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
|
||||
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip,
|
||||
@@ -172,17 +160,34 @@ sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
|
||||
</details>
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
|
||||
cd mocktail
|
||||
git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap
|
||||
make build
|
||||
./build/mocktail
|
||||
```
|
||||
|
||||
To install it for your user, with an app menu entry and Roblox link handling:
|
||||
|
||||
```bash
|
||||
make install PREFIX=~/.local
|
||||
```
|
||||
|
||||
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 run the same checks as CI before each commit and push, install
|
||||
[lefthook](https://github.com/evilmartians/lefthook) and run once:
|
||||
|
||||
```bash
|
||||
lefthook install
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
Mocktail does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Mocktail to fail to start or crash during runtime.
|
||||
Run Mocktail without `hardened_malloc` enabled.
|
||||
Nightcap does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Nightcap to fail to start or crash during runtime.
|
||||
Run Nightcap without `hardened_malloc` enabled.
|
||||
|
||||
## License
|
||||
|
||||
@@ -190,7 +195,12 @@ Run Mocktail without `hardened_malloc` enabled.
|
||||
|
||||
## Support
|
||||
|
||||
You can support the project by giving it a star or with cryptocurrency:
|
||||
Support this fork by giving it a star or with cryptocurrency:
|
||||
|
||||
- LTC: `LdABR2ELRYrUESWZWrEk1uNx38gHHMuHUU`
|
||||
- SOL: `tjeDnPoWeyW8zfTf8c5CVAm5QTV8vFPaPouaScAdwfT`
|
||||
|
||||
Support the upstream project, which does most of the heavy lifting:
|
||||
|
||||
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
|
||||
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
@@ -12,7 +13,7 @@ find_path(MOCKTAIL_EGL_INCLUDE_DIR EGL/egl.h REQUIRED)
|
||||
find_path(MOCKTAIL_GLES3_INCLUDE_DIR GLES3/gl3.h REQUIRED)
|
||||
|
||||
set(MOCKTAIL_WINDOW_ICON_PNG
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/packaging/icons/hicolor/48x48/apps/space.bigrat.mocktail.png"
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/packaging/icons/hicolor/48x48/apps/io.github.CoderDayton.nightcap.png"
|
||||
)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${MOCKTAIL_WINDOW_ICON_PNG}"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Mocktail configuration.
|
||||
# The file is created once with mode 0600. Mocktail never replaces your edits.
|
||||
|
||||
@@ -46,7 +47,9 @@ graphics:
|
||||
# display, unlimited, or any positive integer; fixed values are forwarded to
|
||||
# DFIntTaskSchedulerTargetFps without a whitelist.
|
||||
# frame_rate_limit: -1
|
||||
# Optional presentation synchronization override: auto, on, or off.
|
||||
# Optional presentation synchronization override: auto, on, or off. auto
|
||||
# presents through the lowest-latency synchronized mode the driver offers
|
||||
# unless frame_rate_limit is unlimited.
|
||||
# vsync: off
|
||||
|
||||
performance:
|
||||
@@ -74,6 +77,14 @@ audio:
|
||||
# between boots; prefer the exact device name when it is unique.
|
||||
input_device: default
|
||||
|
||||
input:
|
||||
# Boolean (default: false): send mouse look deltas in the host's own units.
|
||||
# They are otherwise scaled by the window's pixel density divided by the
|
||||
# desktop display scale, which cancels to 1.0 unless fractional scaling makes
|
||||
# the two disagree. Turn this on when first-person aiming feels too slow or
|
||||
# too fast under fractional scaling. The pointer position is unaffected.
|
||||
raw_mouse: false
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
@@ -86,9 +97,9 @@ integrations:
|
||||
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
|
||||
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# Boolean (default: true): publish Nightcap activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
enabled: false
|
||||
enabled: true
|
||||
# Boolean (default: true): show the current Roblox experience name.
|
||||
show_place_name: true
|
||||
# Boolean (default: true): show how long the current session has run.
|
||||
@@ -101,12 +112,24 @@ integrations:
|
||||
public_servers_only: true
|
||||
# button_label: Join Server
|
||||
# English defaults. Uncomment only the lines you want to customize.
|
||||
# Text and image fields accept {place_name}, {place_icon}, and
|
||||
# {creator_name}. A field that expands an empty placeholder is left out,
|
||||
# and a "" title shows the app name.
|
||||
# title, state, and the images fields may be "" to hide them.
|
||||
# text:
|
||||
# browsing: Browsing experiences
|
||||
# joining: Joining an experience
|
||||
# playing: "{place_name}"
|
||||
# state: Playing Roblox
|
||||
# playing: "Playing {place_name}"
|
||||
# state: "by {creator_name}"
|
||||
# unknown_place: Unknown experience
|
||||
# title: Roblox
|
||||
# Images take an https:// URL, or an asset key uploaded to application_id.
|
||||
# large: "" hides both images; the small image needs a large one.
|
||||
# images:
|
||||
# large: "{place_icon}"
|
||||
# large_text: "{place_name}"
|
||||
# small: roblox_small
|
||||
# small_text: Roblox
|
||||
# Advanced: overrides the application bundled by the package maintainer.
|
||||
# application_id: 123456789012345678
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Benchmarking
|
||||
|
||||
A pull request that claims a speedup shows before and after numbers taken
|
||||
the way this guide describes. Numbers from different machines are not
|
||||
compared with each other. Each run is compared with a run on the same
|
||||
machine.
|
||||
|
||||
## Two layers, two tools
|
||||
|
||||
Roblox measures its own work. Nightcap measures the layer between Roblox and
|
||||
your driver.
|
||||
|
||||
| Layer | What it covers | Tool |
|
||||
| --- | --- | --- |
|
||||
| Roblox | Scripts, physics, rendering jobs, network | Roblox's stats overlays, Shift+F4 and Shift+F5 in game |
|
||||
| Nightcap | Vulkan adapter: present, swapchain and GPU waits, submits, ETC2 texture decode, pipeline and shader creation | `--profile` |
|
||||
|
||||
A Nightcap change should move the `--profile` numbers. If it only moves the
|
||||
Roblox overlay numbers, say so in the pull request, since the cause is then
|
||||
less direct.
|
||||
|
||||
## Recording a trace
|
||||
|
||||
```sh
|
||||
mocktail --profile ~/nightcap-before.json
|
||||
```
|
||||
|
||||
The Vulkan adapter writes a Chrome-format trace to that file, a few times a
|
||||
second. The file is valid JSON after every write, so a session that crashes
|
||||
or is killed still leaves a readable trace. Exit from the Roblox menu when you are done. Open the file
|
||||
in [ui.perfetto.dev](https://ui.perfetto.dev) or `chrome://tracing`.
|
||||
|
||||
Expect about 130 bytes per event, around 15 MB per minute at 240 FPS.
|
||||
Profiling is off unless `--profile` is given, and `MOCKTAIL_PROFILE_TRACE`
|
||||
does the same thing as the flag.
|
||||
|
||||
### What the trace contains
|
||||
|
||||
Each slice is named after the call it times, on the thread that made it.
|
||||
|
||||
| Category | Slices | Arguments |
|
||||
| --- | --- | --- |
|
||||
| `present` | `vkQueuePresentKHR`: the whole adapter present, including the text overlay | `result` |
|
||||
| `present` | `host present`: the host driver's present, nested inside the slice above | |
|
||||
| `wait` | `vkAcquireNextImageKHR`, `vkAcquireNextImage2KHR`, `vkWaitForFences`, `vkWaitSemaphores`, `vkWaitSemaphoresKHR`, `vkQueueWaitIdle`, `vkDeviceWaitIdle` | |
|
||||
| `submit` | `vkQueueSubmit`, `vkQueueSubmit2`, `vkQueueSubmit2KHR` | `submits` |
|
||||
| `texture` | `etc2 decode`: CPU decode of ETC2 textures before a submit | `uploads`, `compressed_bytes`, `decoded_bytes` |
|
||||
| `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. | |
|
||||
|
||||
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.
|
||||
|
||||
## The benchmark runs
|
||||
|
||||
Run every scenario before and after the change, on the same build type
|
||||
(`make build`), with the same `config.yaml`. Close other GPU-heavy apps.
|
||||
Record `graphics.frame_rate_limit` and `graphics.vsync` in the pull request,
|
||||
because a frame cap hides frame time gains.
|
||||
|
||||
| Scenario | How to start | What to do | What it shows |
|
||||
| --- | --- | --- | --- |
|
||||
| Home screen, cold | `mocktail --profile home-cold.json` with an empty shader cache (below) | Wait 30 s on the home screen, exit | First-load stutter: `pipeline` and `texture` slices |
|
||||
| Tower of Hell | `mocktail --profile toh.json --launch-uri "roblox://experiences/start?placeId=1962086868"` | Stand still at spawn for 60 s, exit | Steady-state frame time on a light scene |
|
||||
| Murder Mystery 2 | `mocktail --profile mm2.json --launch-uri "roblox://experiences/start?placeId=142823291"` | Stay in the lobby for 60 s, exit | Frame time with many players and a busier scene |
|
||||
|
||||
For the cold run, point the driver's shader cache at an empty directory, so
|
||||
your normal cache is left alone:
|
||||
|
||||
```sh
|
||||
cache=$(mktemp -d)
|
||||
MESA_SHADER_CACHE_DIR="$cache" __GL_SHADER_DISK_CACHE_PATH="$cache" \
|
||||
mocktail --profile home-cold.json
|
||||
```
|
||||
|
||||
`MESA_SHADER_CACHE_DIR` applies to Mesa drivers (AMD, Intel) and
|
||||
`__GL_SHADER_DISK_CACHE_PATH` to the NVIDIA driver.
|
||||
|
||||
Run each scenario three times and keep the median run, since one run can
|
||||
catch a background stall.
|
||||
|
||||
## Comparing runs
|
||||
|
||||
```sh
|
||||
scripts/summarize_profile_trace.py toh-before.json toh-after.json
|
||||
```
|
||||
|
||||
This prints a Markdown table with one column per trace: frame count, frame
|
||||
interval mean, p50, p95, p99 and max, then count, total, mean, p99 and max
|
||||
for every slice name, largest total first. Paste the table into the pull
|
||||
request and attach both traces.
|
||||
|
||||
For frame time, compare p95 and p99 as well as the mean. Stutter shows up in
|
||||
the tail first. For first-load work, compare the `total (ms)` rows of the
|
||||
`pipeline` and `texture` slices from the cold run.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Performance and CPU load
|
||||
|
||||
What Nightcap does to your CPU and GPU, which knobs change it, and how to
|
||||
measure rather than guess.
|
||||
|
||||
## GameMode core pinning
|
||||
|
||||
Nightcap asks Feral GameMode for a performance session at startup
|
||||
(`performance.gamemode`, default `auto`). GameMode raises the CPU governor and
|
||||
the process priority, which helps.
|
||||
|
||||
On a CPU with a wide spread of per-core maximum frequencies — Intel P-core and
|
||||
E-core parts, AMD parts with preferred cores — GameMode also pins the process
|
||||
to the fastest cores. On a 32-thread host that can mean 4 usable threads out of
|
||||
32. Roblox spreads its work across a worker pool, so the pin concentrates that
|
||||
pool onto a couple of physical cores instead of spreading it. Those cores sit
|
||||
near their boost ceiling and run hot while the rest of the chip idles.
|
||||
|
||||
GameMode re-applies the pin roughly every 5 seconds, so widening the mask from
|
||||
inside the process does not hold. Releasing the request does release the pin.
|
||||
|
||||
Under `auto`, Nightcap therefore checks its CPU affinity immediately after the
|
||||
request. If GameMode narrowed it, Nightcap ends the request and prints:
|
||||
|
||||
```
|
||||
[gamemode] released: it pinned this process to a subset of the CPUs, ...
|
||||
```
|
||||
|
||||
The trade is that GameMode's governor and priority boost go with it. To keep
|
||||
those and lose only the pinning, turn pinning off for every game on the host:
|
||||
|
||||
```ini
|
||||
# ~/.config/gamemode.ini
|
||||
[cpu]
|
||||
pin_cores=no
|
||||
park_cores=no
|
||||
```
|
||||
|
||||
GameMode reads `$XDG_CONFIG_HOME`, `/usr/share/gamemode` and `/etc`, in that
|
||||
order. It does not read anything under Nightcap's own config directory, so this
|
||||
file has to live in one of those three places. The daemon reads it when it
|
||||
starts, so restart it after editing: `systemctl --user restart gamemoded`.
|
||||
|
||||
The governor switch runs through polkit. Debian and Ubuntu only allow it for
|
||||
members of the `gamemode` group; without that, the daemon's journal shows
|
||||
`cpugovctl set performance: Not authorized` and the governor stays put while
|
||||
the session is otherwise active. Add yourself and log in again:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG gamemode "$USER"
|
||||
```
|
||||
|
||||
Fedora and Arch allow any logged-in local user.
|
||||
|
||||
Setting `performance.gamemode: on` accepts the pinning instead: `on` is an
|
||||
explicit request for GameMode, and Nightcap keeps the session whatever it does
|
||||
to the affinity. `off` never contacts the daemon.
|
||||
|
||||
To see which CPUs the process may use:
|
||||
|
||||
```bash
|
||||
taskset -pc $(pgrep -x mocktail)
|
||||
```
|
||||
|
||||
## The main-thread pump
|
||||
|
||||
Roblox posts its per-frame main-thread step at a time of its own choosing,
|
||||
and nothing signals the host when it does. The host main thread therefore
|
||||
polls `nativeCallMessagesFromMainThread`, and a post that waits more than a
|
||||
fraction of a millisecond costs a whole frame. Between polls the thread rests
|
||||
for 100 µs, and how it rests decides the CPU clock:
|
||||
|
||||
| Mode | When | What it costs |
|
||||
| --- | --- | --- |
|
||||
| `sleep` | The governor is `performance`, which GameMode sets | The thread idles at about 3% of a core. The governor holds the clock. |
|
||||
| `tpause` | Otherwise, on a CPU with WAITPKG (Intel 12th gen and newer) | The core stays awake in a light C0 state. Frame rate holds; the OS books the time as busy, so the thread shows around 70% of a core, at lower power than a spin. |
|
||||
| `spin` | Otherwise | The core stays awake at full power. |
|
||||
|
||||
Under a power-saving governor the CPU clocks a bursty core down unless some
|
||||
core stays awake, and Roblox's render thread then runs slower and drops
|
||||
frames. That is why `sleep` is not the default there. Startup prints the
|
||||
chosen mode:
|
||||
|
||||
```
|
||||
[main] engine pump rest: tpause (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)
|
||||
```
|
||||
|
||||
`MOCKTAIL_ENGINE_PUMP_REST` forces a mode. `sleep` without the governor's help
|
||||
runs cooler for a few frames per second less; measure before keeping it.
|
||||
|
||||
## Small texture upscaling
|
||||
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) redraws ETC2 textures of 64px or
|
||||
smaller at N times their width and height. At the default that is 16 times the
|
||||
pixels per affected texture.
|
||||
|
||||
The cost lands in two places. Decoding and resampling is CPU work on up to 8
|
||||
threads while textures load, which shows up as a burst rather than a steady
|
||||
load. The enlarged textures then stay in GPU memory for the session and cost
|
||||
extra sampling bandwidth on every frame that uses them.
|
||||
|
||||
Set it to `1` to turn upscaling off:
|
||||
|
||||
```bash
|
||||
MOCKTAIL_SMALL_TEXTURE_UPSCALE=1 mocktail
|
||||
```
|
||||
|
||||
## Frame rate and present mode
|
||||
|
||||
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
|
||||
framerate cap in charge; Nightcap sets no `DFIntTaskSchedulerTargetFps`
|
||||
override. A fixed value forwards that number to the scheduler.
|
||||
|
||||
`graphics.vsync` (default `auto`) selects the presentation mode. `auto` and
|
||||
`on` both pick the lowest-latency synchronized mode the driver offers, in
|
||||
order: `FIFO_LATEST_READY`, `MAILBOX`, `FIFO_RELAXED`, `FIFO`. The first two do
|
||||
not block the render thread on the display refresh, so the frame rate is
|
||||
governed by the in-game cap rather than by presentation. `off` selects
|
||||
`IMMEDIATE` and does not synchronize at all.
|
||||
|
||||
If the frame rate is higher than your refresh rate, the extra frames are work
|
||||
you cannot see. Lower the in-game cap to your refresh rate first.
|
||||
|
||||
## Measuring
|
||||
|
||||
Shift+F4 in game shows Roblox's own scheduler jobs, with a millisecond cost and
|
||||
a percentage for each. It does not show Nightcap's threads — the Vulkan
|
||||
adapter, the ETC2 decode workers, the libc shim — so it is a reading on the
|
||||
game, not on the process.
|
||||
|
||||
For the whole process:
|
||||
|
||||
```bash
|
||||
top -H -p $(pgrep -x mocktail) # per-thread CPU, sorted
|
||||
ps -L -o psr=,pcpu=,comm= -p $(pgrep -x mocktail) # which core each thread is on
|
||||
nvidia-smi --query-gpu=utilization.gpu,temperature.gpu,power.draw --format=csv
|
||||
```
|
||||
|
||||
A thread pinned near 100% while the frame rate is steady is a busy-wait. The
|
||||
one exception is the host main thread in `tpause` mode, which the OS reports
|
||||
as busy while the core is parked (see above). Load spread across many
|
||||
`RBX Worker` threads is the game's own scheduler, and its cost tracks the
|
||||
place you are in — part count, moving parts and joints all show in the
|
||||
Shift+F4 World section.
|
||||
|
||||
`--profile <file>` writes a Chrome trace of the Vulkan adapter's work for
|
||||
ui.perfetto.dev. See [BENCHMARKING.md](BENCHMARKING.md).
|
||||
@@ -0,0 +1,90 @@
|
||||
# Nightcap roadmap
|
||||
|
||||
What Nightcap is working toward, in priority order. Items move to
|
||||
[Shipped](#shipped) when they land on `main`. Open an issue to propose a
|
||||
change to this list.
|
||||
|
||||
## 1. Settings UI
|
||||
|
||||
Nightcap is configured by editing `~/.config/mocktail/config.yaml`. Most
|
||||
people never open it, so they never find the FPS unlock or the vsync switch.
|
||||
|
||||
Goal: a small GTK4 settings window, reachable from the app menu and from a
|
||||
`--settings` flag, that reads and writes the same `config.yaml`.
|
||||
|
||||
- Graphics tab: backend, frame rate limit, vsync, small texture upscale.
|
||||
- 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.
|
||||
- Changes apply on next launch. A restart button restarts Nightcap.
|
||||
|
||||
Done when every option in `config/mocktail.example.yaml` that a player would
|
||||
touch can be set without a text editor.
|
||||
|
||||
## 2. Controller button glyphs
|
||||
|
||||
Upstream added Xbox and PlayStation gamepad support through SDL. Controllers
|
||||
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.
|
||||
|
||||
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.
|
||||
- Mouse look keeps responding after a long turn, instead of going dead until
|
||||
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.
|
||||
- Small textures upscaled 4x by default, with PNG overrides in
|
||||
`~/.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.
|
||||
- Custom Discord presence: title, details, state, and large and small icons
|
||||
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.
|
||||
- Texture uploads no longer stall the frame they land on: ETC2 decode and
|
||||
resampling run on a persistent worker pool.
|
||||
- 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).
|
||||
- Own app ID `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream.
|
||||
- AppImage and signed Flatpak releases on every `v*` tag, with the Flatpak
|
||||
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.
|
||||
@@ -0,0 +1,121 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
#define MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Environment variable naming the trace file the Vulkan adapter writes.
|
||||
inline constexpr char kProfileTraceEnvironment[] = "MOCKTAIL_PROFILE_TRACE";
|
||||
|
||||
struct TraceArg {
|
||||
const char* key;
|
||||
std::int64_t value;
|
||||
};
|
||||
|
||||
// CLOCK_MONOTONIC in nanoseconds.
|
||||
std::uint64_t TraceClockNanos();
|
||||
|
||||
// Writes the Trace Event Format JSON array read by chrome://tracing and
|
||||
// ui.perfetto.dev. Recording threads only format into a shared buffer; a
|
||||
// background thread owns file writes, so disk stalls do not land inside the
|
||||
// measured calls. Events are written in arrival order. Each write ends the
|
||||
// file with the closing bracket, so a killed session leaves a loadable trace
|
||||
// holding everything written before it died.
|
||||
// Names, categories, and keys must outlive the call.
|
||||
class ChromeTraceWriter final {
|
||||
public:
|
||||
static std::unique_ptr<ChromeTraceWriter> Open(
|
||||
const std::filesystem::path& path, std::string* error);
|
||||
|
||||
~ChromeTraceWriter();
|
||||
ChromeTraceWriter(const ChromeTraceWriter&) = delete;
|
||||
ChromeTraceWriter& operator=(const ChromeTraceWriter&) = delete;
|
||||
|
||||
// A complete ("X") slice on the calling thread. Slices on one thread must
|
||||
// nest, which scoped timing gives.
|
||||
void Slice(const char* name, const char* category, std::uint64_t start_ns,
|
||||
std::uint64_t end_ns, std::initializer_list<TraceArg> args = {});
|
||||
void Slice(const char* name, const char* category, std::uint64_t start_ns,
|
||||
std::uint64_t end_ns, const TraceArg* args,
|
||||
std::size_t arg_count);
|
||||
// A counter ("C") sample; each name becomes one counter track.
|
||||
void Counter(const char* name, std::uint64_t timestamp_ns, double value);
|
||||
|
||||
// Returns once every event recorded before the call is on disk.
|
||||
void Flush();
|
||||
// Flushes, closes the file, and drops later events. Idempotent.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
explicit ChromeTraceWriter(std::FILE* file);
|
||||
void Append(const char* event, std::size_t size);
|
||||
void WriterLoop();
|
||||
void WritePending();
|
||||
|
||||
std::FILE* file_;
|
||||
const int owner_pid_;
|
||||
std::mutex file_mutex_; // Guards file_ and spare_.
|
||||
std::string spare_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
std::string pending_;
|
||||
bool first_event_ = true;
|
||||
bool closed_ = false;
|
||||
bool stopping_ = false;
|
||||
std::thread writer_;
|
||||
};
|
||||
|
||||
// The process trace named by MOCKTAIL_PROFILE_TRACE, opened on first use.
|
||||
// Null when profiling is off or the file could not be opened. The writer is
|
||||
// never destroyed; it is closed when the process exits normally.
|
||||
ChromeTraceWriter* ActiveProfileTrace();
|
||||
|
||||
// Times its own lifetime as one slice. A null writer records nothing.
|
||||
class TraceScope final {
|
||||
public:
|
||||
TraceScope(ChromeTraceWriter* writer, const char* name,
|
||||
const char* category)
|
||||
: writer_(writer),
|
||||
name_(name),
|
||||
category_(category),
|
||||
start_ns_(writer != nullptr ? TraceClockNanos() : 0) {}
|
||||
~TraceScope() {
|
||||
if (writer_ != nullptr) {
|
||||
writer_->Slice(name_, category_, start_ns_, TraceClockNanos(), args_,
|
||||
arg_count_);
|
||||
}
|
||||
}
|
||||
TraceScope(const TraceScope&) = delete;
|
||||
TraceScope& operator=(const TraceScope&) = delete;
|
||||
|
||||
// Keeps the first kMaxArgs arguments.
|
||||
void Arg(const char* key, std::int64_t value) {
|
||||
if (writer_ != nullptr && arg_count_ < kMaxArgs) {
|
||||
args_[arg_count_++] = {key, value};
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kMaxArgs = 4;
|
||||
ChromeTraceWriter* writer_;
|
||||
const char* name_;
|
||||
const char* category_;
|
||||
std::uint64_t start_ns_;
|
||||
TraceArg args_[kMaxArgs]{};
|
||||
std::size_t arg_count_ = 0;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// ETC2 and EAC block encodings from the OpenGL ES 3.0 / Vulkan core formats.
|
||||
// sRGB variants share the unorm encoding; the colour space belongs to the
|
||||
// destination format.
|
||||
enum class EtcFormat {
|
||||
kEtc2Rgb8,
|
||||
kEtc2Rgb8A1,
|
||||
kEtc2Rgba8,
|
||||
kEacR11,
|
||||
kEacR11Signed,
|
||||
kEacRg11,
|
||||
kEacRg11Signed,
|
||||
};
|
||||
|
||||
// Compressed bytes per 4x4 block.
|
||||
std::size_t EtcBlockBytes(EtcFormat format);
|
||||
|
||||
// Decoded bytes per texel: 4 (RGBA8) for ETC2 formats, 2 per channel
|
||||
// (16-bit, little-endian) for EAC formats.
|
||||
std::size_t EtcDecodedTexelBytes(EtcFormat format);
|
||||
|
||||
// Decodes a width x height image stored as ceil(width/4) x ceil(height/4)
|
||||
// blocks into row-major texels. Returns false, writing nothing, when a buffer
|
||||
// is smaller than the image requires.
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
// Decodes block rows [first_block_row, first_block_row + block_row_count) of
|
||||
// the image DecodeEtcImage describes, writing only the texel rows those blocks
|
||||
// cover. Bands that together cover every block row produce DecodeEtcImage's
|
||||
// output. Returns false, writing nothing, when a buffer is too small or the
|
||||
// rows lie outside the image.
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
// One DecodeEtcImage call. Jobs must not share destination bytes.
|
||||
struct EtcDecodeJob {
|
||||
EtcFormat format = EtcFormat::kEtc2Rgb8;
|
||||
const std::uint8_t* source = nullptr;
|
||||
std::size_t source_bytes = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint8_t* destination = nullptr;
|
||||
std::size_t destination_bytes = 0;
|
||||
// Set to DecodeEtcImage's result.
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// Decodes every job before returning. Large batches are split into block-row
|
||||
// bands decoded on up to worker_count threads, the caller included; small
|
||||
// batches decode on the caller only.
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count);
|
||||
|
||||
// Calls run(context) worker_count times, once on the caller and the rest on
|
||||
// pooled decode workers that run alongside it, and returns when every call
|
||||
// has returned. `run` must be safe to call from several threads at once, so
|
||||
// it should drain a shared queue. When the pool is busy with another batch,
|
||||
// or no worker could be started, run is called on the caller alone.
|
||||
void RunOnDecodeWorkers(void (*run)(void*), void* context,
|
||||
unsigned worker_count);
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
#define MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// Tightly packed RGBA8 pixels, row-major, top row first.
|
||||
struct RgbaImage {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
// FNV-1a over the bytes; the texture identity used for dump and override
|
||||
// file names.
|
||||
std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size);
|
||||
// 16 lowercase hex digits.
|
||||
std::string HashName(std::uint64_t hash);
|
||||
|
||||
bool ReadPngRgba(const std::string& path, RgbaImage* image);
|
||||
bool WritePngRgba(const std::string& path, const RgbaImage& image);
|
||||
|
||||
// Writes width x height RGBA8 pixels to destination, each destination pixel
|
||||
// the average of the source box it covers. Growing repeats source pixels.
|
||||
void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
|
||||
// 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
|
||||
// directory, every level-0 texture seen is written once as
|
||||
// <dump_dir>/<hash>_<width>x<height>.png so the hash can be found.
|
||||
// Thread-safe.
|
||||
class TextureOverrides {
|
||||
public:
|
||||
TextureOverrides(std::string override_dir, std::string dump_dir);
|
||||
// MOCKTAIL_TEXTURE_OVERRIDE_DIR and MOCKTAIL_TEXTURE_DUMP_DIR.
|
||||
static TextureOverrides FromEnvironment();
|
||||
|
||||
bool enabled() const { return !override_dir_.empty() || !dump_dir_.empty(); }
|
||||
bool dumping() const { return !dump_dir_.empty(); }
|
||||
|
||||
// Null when no override file exists; lookups are cached either way.
|
||||
std::shared_ptr<const RgbaImage> Lookup(std::uint64_t hash);
|
||||
void Dump(std::uint64_t hash, std::uint32_t width, std::uint32_t height,
|
||||
const std::uint8_t* rgba);
|
||||
|
||||
private:
|
||||
std::string override_dir_;
|
||||
std::string dump_dir_;
|
||||
std::mutex mutex_;
|
||||
std::unordered_map<std::uint64_t, std::shared_ptr<const RgbaImage>> cache_;
|
||||
std::unordered_set<std::uint64_t> dumped_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
@@ -0,0 +1,131 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
#define MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// Host format that stores a decoded ETC2/EAC image, with its block encoding.
|
||||
struct Etc2EmulatedFormat {
|
||||
VkFormat host_format = VK_FORMAT_UNDEFINED;
|
||||
EtcFormat etc_format = EtcFormat::kEtc2Rgb8;
|
||||
};
|
||||
|
||||
// Maps an ETC2/EAC Vulkan format to its decoded host format. Returns false for
|
||||
// every other format.
|
||||
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out);
|
||||
|
||||
// Format features reported for an emulated ETC2/EAC format: the host format's
|
||||
// sampling and transfer features only, as for a native compressed format.
|
||||
VkFormatFeatureFlags EmulatedEtc2FormatFeatures(
|
||||
VkFormatFeatureFlags host_features);
|
||||
|
||||
// Staging bytes consumed and decoded bytes produced by one 2D upload region.
|
||||
// Returns false for an empty extent, a depth other than 1, or no layers.
|
||||
bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t layer_count, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded);
|
||||
|
||||
// Whether emulated ETC2 is reported to the application as a supported
|
||||
// feature, from the MOCKTAIL_ADVERTISE_ETC2 value: "0" hides it, anything
|
||||
// else (or unset) reports it. Hidden ETC2 still decodes ETC2 images the
|
||||
// application creates; it only steers the application toward other formats.
|
||||
bool Etc2SupportAdvertised(const char* value);
|
||||
bool Etc2SupportAdvertised();
|
||||
|
||||
// Factor applied to emulated ETC2 colour images no larger than
|
||||
// kSmallTextureMaxExtent, from the MOCKTAIL_SMALL_TEXTURE_UPSCALE value:
|
||||
// 1 leaves them alone; 2..8 create them that many times larger and resample
|
||||
// every upload, so a texture override can carry more detail than the
|
||||
// original. Unset or non-numeric values use kDefaultSmallTextureUpscale;
|
||||
// numeric values out of range are clamped.
|
||||
inline constexpr std::uint32_t kSmallTextureMaxExtent = 64;
|
||||
inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4;
|
||||
std::uint32_t SmallTextureUpscale(const char* value);
|
||||
|
||||
// Reports ETC2/EAC support on hosts without native ETC2 (desktop GPUs) and
|
||||
// stores those textures decoded. Images are created with the host format;
|
||||
// each staged upload into one is redirected to a host-visible staging buffer
|
||||
// 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.
|
||||
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
|
||||
class VulkanEtc2Emulation final {
|
||||
public:
|
||||
VulkanEtc2Emulation();
|
||||
~VulkanEtc2Emulation();
|
||||
VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete;
|
||||
VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete;
|
||||
|
||||
// True when this physical device lacks native ETC2 and emulation is on.
|
||||
bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device,
|
||||
PFN_vkGetPhysicalDeviceFeatures host);
|
||||
|
||||
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
|
||||
bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image);
|
||||
void DestroyImage(VkDevice device, VkImage image,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
VkResult CreateImageView(VkDevice device,
|
||||
const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkImageView* view);
|
||||
VkResult BindBufferMemory(VkDevice device, VkBuffer buffer,
|
||||
VkDeviceMemory memory, VkDeviceSize offset);
|
||||
VkResult BindBufferMemory2(VkDevice device, std::uint32_t count,
|
||||
const VkBindBufferMemoryInfo* infos);
|
||||
VkResult MapMemory(VkDevice device, VkDeviceMemory memory,
|
||||
VkDeviceSize offset, VkDeviceSize size,
|
||||
VkMemoryMapFlags flags, void** data);
|
||||
VkResult MapMemory2(VkDevice device, const VkMemoryMapInfo* info,
|
||||
void** data);
|
||||
void UnmapMemory(VkDevice device, VkDeviceMemory memory);
|
||||
VkResult UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo* info);
|
||||
void FreeMemory(VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void DestroyBuffer(VkDevice device, VkBuffer buffer,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void CmdCopyBufferToImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkBuffer source, VkImage destination,
|
||||
VkImageLayout layout, std::uint32_t region_count,
|
||||
const VkBufferImageCopy* regions);
|
||||
void CmdCopyBufferToImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyBufferToImageInfo2* info);
|
||||
void CmdCopyImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkImage source, VkImageLayout source_layout,
|
||||
VkImage destination, VkImageLayout destination_layout,
|
||||
std::uint32_t region_count, const VkImageCopy* regions);
|
||||
void CmdCopyImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyImageInfo2* info);
|
||||
void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer,
|
||||
std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries);
|
||||
|
||||
// Decodes every upload recorded into these command buffers and the
|
||||
// secondary command buffers they execute. Large batches decode on worker
|
||||
// threads; this returns once every upload is decoded.
|
||||
void PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count);
|
||||
// Destroys the staging buffers owned by a command buffer that is no longer
|
||||
// pending.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
struct State;
|
||||
|
||||
private:
|
||||
State* state_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
|
||||
@@ -6,9 +7,9 @@
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
inline constexpr char kMocktailApplicationName[] = "Mocktail";
|
||||
inline constexpr char kMocktailApplicationName[] = "Nightcap";
|
||||
inline constexpr char kMocktailApplicationIdentifier[] =
|
||||
"space.bigrat.mocktail";
|
||||
"io.github.CoderDayton.nightcap";
|
||||
|
||||
// Configures the stable compositor identity before SDL initializes. The
|
||||
// identifier matches the installed desktop file and icon name so Wayland and
|
||||
|
||||
@@ -33,6 +33,8 @@ struct CommandLineOptions {
|
||||
// Requests one explicit launch of the provider latest without promoting it
|
||||
// into the managed payload state.
|
||||
bool force_run_latest = false;
|
||||
// Chrome trace file the Vulkan adapter writes; empty when not profiling.
|
||||
std::string profile_trace_path;
|
||||
};
|
||||
|
||||
struct CommandLineParseResult {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
#define MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Android ActivityManager memory values published through DeviceParams.
|
||||
// Roblox derives its device RAM tier, and tiered render budgets, from them.
|
||||
struct DeviceMemoryProfile {
|
||||
std::int32_t total_memory_mb = 0;
|
||||
std::int32_t memory_class_mb = 0;
|
||||
std::int32_t large_memory_class_mb = 0;
|
||||
std::int64_t low_memory_killer_background_threshold = 0;
|
||||
std::int64_t low_memory_killer_foreground_threshold = 0;
|
||||
bool low_ram_device = false;
|
||||
};
|
||||
|
||||
// Hosts with at least 4 GiB report their real total memory as a normal-RAM
|
||||
// device. Smaller hosts, or an unreadable total (0), keep the 2 GiB low-RAM
|
||||
// profile.
|
||||
DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
@@ -13,11 +13,14 @@ namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DiscordRpcActivity {
|
||||
std::string name;
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::optional<int64_t> start_timestamp;
|
||||
std::string large_image;
|
||||
std::string large_text;
|
||||
std::string small_image;
|
||||
std::string small_text;
|
||||
std::string button_label;
|
||||
std::string button_url;
|
||||
};
|
||||
@@ -27,7 +30,8 @@ std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request);
|
||||
DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request, std::string place_name,
|
||||
int64_t session_started_at, std::string place_icon_url = {});
|
||||
int64_t session_started_at, std::string place_icon_url = {},
|
||||
std::string creator_name = {});
|
||||
|
||||
// Maintains a direct, unauthenticated Discord Desktop RPC connection. The
|
||||
// worker owns all network and IPC work; lifecycle notifications only replace
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
#define MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Host display or window size in physical pixels, published to the Android
|
||||
// guest. Header-only so the JNI VM can use it without link dependencies.
|
||||
struct DisplaySize {
|
||||
std::int32_t width = 1920;
|
||||
std::int32_t height = 1080;
|
||||
};
|
||||
|
||||
// Environment handoffs written after the host window exists, as
|
||||
// "WIDTHxHEIGHT": the window's display mode and the window's initial pixels.
|
||||
inline constexpr const char* kDisplaySizeEnvironment =
|
||||
"MOCKTAIL_DISPLAY_SIZE_INTERNAL";
|
||||
inline constexpr const char* kWindowSizeEnvironment =
|
||||
"MOCKTAIL_WINDOW_SIZE_INTERNAL";
|
||||
|
||||
// Parses "WIDTHxHEIGHT". A null, malformed or non-positive value, or one that
|
||||
// does not fit a Java int, yields the 1920x1080 fallback.
|
||||
inline DisplaySize ParseDisplaySize(const char* value) {
|
||||
const DisplaySize fallback;
|
||||
if (value == nullptr) {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parts[2] = {0, 0};
|
||||
const char* cursor = value;
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
if (*cursor < '0' || *cursor > '9') {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parsed = 0;
|
||||
while (*cursor >= '0' && *cursor <= '9') {
|
||||
parsed = parsed * 10 + (*cursor - '0');
|
||||
if (parsed > std::numeric_limits<std::int32_t>::max()) {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
if (parsed == 0) {
|
||||
return fallback;
|
||||
}
|
||||
parts[index] = parsed;
|
||||
if (index == 0) {
|
||||
if (*cursor != 'x') {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
if (*cursor != '\0') {
|
||||
return fallback;
|
||||
}
|
||||
return {static_cast<std::int32_t>(parts[0]),
|
||||
static_cast<std::int32_t>(parts[1])};
|
||||
}
|
||||
|
||||
// Physical length of a pixel span at the guest's fixed 160 dpi, rounded down
|
||||
// to whole millimetres.
|
||||
inline std::int32_t PixelsToMillimetersAt160Dpi(std::int32_t pixels) {
|
||||
return static_cast<std::int32_t>(static_cast<std::int64_t>(pixels) * 254 /
|
||||
1600);
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
#define MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
// The engine posts its per-frame main-thread step at a time of its own
|
||||
// choosing, with no signal the host can block on, so the host main thread
|
||||
// polls nativeCallMessagesFromMainThread. An empty poll costs a few
|
||||
// microseconds; polled back to back it burns a core.
|
||||
//
|
||||
// Between polls the thread rests for kEnginePumpRestNs. A post is then picked
|
||||
// up at most kEnginePumpRestNs + timer slack + wake latency late, far under
|
||||
// the engine's sub-millisecond slack.
|
||||
//
|
||||
// How it rests decides the clock. Under a power-saving governor the CPU clocks
|
||||
// a bursty core down unless some core stays awake, and the engine's render
|
||||
// thread then runs slower and drops frames. So:
|
||||
// kSleep nanosleep. The core idles. Correct only when the governor holds
|
||||
// the clock up on its own (performance, which GameMode sets).
|
||||
// kTpause TPAUSE in C0.2. The core stays awake at reduced power, the clock
|
||||
// holds, and the OS books the time as busy. Needs WAITPKG.
|
||||
// kSpin No rest. The core stays awake at full power.
|
||||
inline constexpr uint64_t kEnginePumpRestNs = 100'000;
|
||||
// Timer slack the calling thread is set to; the kernel default of 50 us would
|
||||
// otherwise stretch every sleep.
|
||||
inline constexpr uint64_t kEnginePumpTimerSlackNs = 10'000;
|
||||
// Shallow C-state exit plus scheduler latency on the host, for the bound.
|
||||
inline constexpr uint64_t kShallowWakeNs = 50'000;
|
||||
|
||||
enum class EnginePumpRestMode { kSleep, kTpause, kSpin };
|
||||
|
||||
const char* EnginePumpRestModeName(EnginePumpRestMode mode);
|
||||
|
||||
struct EnginePumpRestInputs {
|
||||
bool governor_performance = false;
|
||||
bool cpu_has_waitpkg = false;
|
||||
// MOCKTAIL_ENGINE_PUMP_REST: "sleep", "tpause" or "spin". Null, empty or
|
||||
// unknown defers to the policy; "tpause" without WAITPKG does too.
|
||||
const char* override = nullptr;
|
||||
};
|
||||
|
||||
EnginePumpRestMode ChooseEnginePumpRestMode(const EnginePumpRestInputs& inputs);
|
||||
|
||||
// True when the file holds "performance". `path` is a cpufreq
|
||||
// scaling_governor file; cpu0's stands for the machine.
|
||||
bool CpuGovernorIsPerformance(const char* path);
|
||||
|
||||
bool CpuHasWaitPkg();
|
||||
|
||||
// The mode chosen from the host on first use.
|
||||
EnginePumpRestMode ActiveEnginePumpRestMode();
|
||||
|
||||
// Rests the calling thread in the given mode and returns the rest length, 0
|
||||
// for kSpin. The first call lowers the thread's timer slack to
|
||||
// kEnginePumpTimerSlackNs.
|
||||
uint64_t RestAfterEnginePump(EnginePumpRestMode mode);
|
||||
inline uint64_t RestAfterEnginePump() {
|
||||
return RestAfterEnginePump(ActiveEnginePumpRestMode());
|
||||
}
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
#define MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
|
||||
#include <sched.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -22,6 +24,7 @@ enum class GameModeSessionState {
|
||||
kDisabled,
|
||||
kUnavailable,
|
||||
kRequestFailed,
|
||||
kDeclinedCorePinning,
|
||||
kAlreadyActive,
|
||||
kActive,
|
||||
kStopped,
|
||||
@@ -43,9 +46,37 @@ struct GameModeClientApi {
|
||||
ErrorFn error_string = nullptr;
|
||||
};
|
||||
|
||||
// The CPU affinity calls, injectable so tests exercise the restore without
|
||||
// moving the threads they run on.
|
||||
struct CpuAffinityApi {
|
||||
// Reads the calling thread's permitted CPUs. GameMode pins every thread
|
||||
// alike, so one thread's mask is enough to detect the pin.
|
||||
using GetFn = int (*)(cpu_set_t* mask);
|
||||
// Applies `mask` to every thread of this process, not only the caller.
|
||||
// Returns 0 when each live thread took it.
|
||||
using SetFn = int (*)(const cpu_set_t* mask);
|
||||
|
||||
GetFn get = nullptr;
|
||||
SetFn set = nullptr;
|
||||
};
|
||||
|
||||
CpuAffinityApi HostCpuAffinityApi();
|
||||
|
||||
// True when `after` permits a strict subset of the CPUs `before` permits.
|
||||
// An equal, wider or merely relocated set is not a narrowing.
|
||||
bool CpuAffinityNarrowed(const cpu_set_t& before, const cpu_set_t& after);
|
||||
|
||||
// Owns at most one ref-counted GameMode request. Missing host support and
|
||||
// daemon rejection remain nonfatal because GameMode is an optimization, not a
|
||||
// correctness dependency.
|
||||
//
|
||||
// GameMode pins the calling process to the host's fastest cores when it sees a
|
||||
// wide core-speed spread. Roblox spreads its own work over a worker pool, so
|
||||
// that pin concentrates the load onto a couple of cores instead of helping,
|
||||
// and the daemon re-applies it every few seconds, so correcting the mask in
|
||||
// place does not hold. Under `auto`, a request that narrows the affinity is
|
||||
// released again: ending the request is what releases the pin. `on` is an
|
||||
// explicit choice and keeps the pinned session.
|
||||
class GameModeSession final {
|
||||
public:
|
||||
GameModeSession() = default;
|
||||
@@ -59,6 +90,8 @@ class GameModeSession final {
|
||||
static GameModeSession Start(GameModePolicy policy);
|
||||
static GameModeSession StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api);
|
||||
static GameModeSession StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api, CpuAffinityApi affinity);
|
||||
|
||||
GameModeSessionState state() const { return state_; }
|
||||
const std::string& detail() const { return detail_; }
|
||||
@@ -73,6 +106,7 @@ class GameModeSession final {
|
||||
private:
|
||||
static GameModeSession StartBound(GameModePolicy policy,
|
||||
GameModeClientApi api,
|
||||
CpuAffinityApi affinity,
|
||||
void* library_handle);
|
||||
void CloseLibrary();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/frame_rate_policy.h"
|
||||
#include "runtime/game_mode.h"
|
||||
@@ -49,18 +51,18 @@ PerformancePolicy ParsePerformancePolicy(
|
||||
// Respects CPU affinity and falls back to the available logical count.
|
||||
int DetectAvailablePhysicalCoreCount();
|
||||
|
||||
// Disabled policy preserves overrides; conflicting values fail.
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
// Disabled policy preserves overrides. A base_json value that differs from
|
||||
// the preset is kept as-is and its key is appended to kept_overrides.
|
||||
bool MergePerformanceClientSettingsOverrides(
|
||||
const PerformancePolicy& policy, std::string_view base_json,
|
||||
std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides = nullptr);
|
||||
|
||||
// Applies mandatory HTTP and crash policies after caller settings.
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
bool MergeRuntimeClientSettingsOverrides(
|
||||
const FrameRatePolicy& frame_rate, const PerformancePolicy& performance,
|
||||
std::string_view base_json, std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides = nullptr);
|
||||
|
||||
// Keeps the legacy test bypass disabled. The host PermissionsProtocol now
|
||||
// answers microphone authorization from capture configuration. No permission
|
||||
|
||||
@@ -123,6 +123,7 @@ class RobloxInputRuntime final {
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
Status GetMainWindowIsMouseLockedCenter(bool* locked_center);
|
||||
bool SupportsGamepads() const;
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
static void PlatformEventCallback(void* context,
|
||||
const platform::PlatformEvent& event);
|
||||
|
||||
@@ -171,6 +171,9 @@ class RobloxInputRouter final {
|
||||
const RobloxTextFocusProperties& properties);
|
||||
RobloxInputDispatchResult HandleEvent(const platform::PlatformEvent& event);
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
// Raw mouse sends look deltas in host units instead of guest surface units.
|
||||
// The reported pointer position stays in guest units either way.
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
struct ActiveTouch {
|
||||
@@ -220,6 +223,10 @@ class RobloxInputRouter final {
|
||||
Status DisconnectGamepadLocked(ActiveGamepad& gamepad);
|
||||
Status DisconnectGamepadsLocked();
|
||||
Status ReleasePressedInputsLocked();
|
||||
// Keeps mouse_x_/mouse_y_ where they are for an event that carries no
|
||||
// usable position, clamped to the current viewport. Before any absolute
|
||||
// position has been seen, the held position is the viewport center.
|
||||
void HoldPointerLocked(const platform::SurfaceCoordinateTransform& transform);
|
||||
int32_t AllocatePointerIdLocked() const;
|
||||
std::vector<ActiveTouch>::iterator FindTouchIteratorLocked(int64_t touch_id,
|
||||
int64_t finger_id);
|
||||
@@ -235,8 +242,14 @@ class RobloxInputRouter final {
|
||||
std::vector<ActiveTouch> active_touches_;
|
||||
std::vector<ActiveKey> active_keys_;
|
||||
std::vector<ActiveGamepad> gamepads_;
|
||||
// Last known pointer position in guest units. Relative-mode motion, and
|
||||
// wheel or button events with zero coordinates, do not move it: the pointer
|
||||
// stays where it was captured while the deltas pass through.
|
||||
float mouse_x_ = 0.0f;
|
||||
float mouse_y_ = 0.0f;
|
||||
// True once an absolute position has been received since Activate.
|
||||
bool pointer_anchored_ = false;
|
||||
bool raw_mouse_ = false;
|
||||
};
|
||||
|
||||
const char* RobloxInputDispatchStateName(RobloxInputDispatchState state);
|
||||
|
||||
@@ -36,6 +36,7 @@ class RobloxWindowInputRuntime final {
|
||||
Status UpdateTextFocusProperties(uint64_t generation,
|
||||
const RobloxTextFocusProperties& properties);
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
static void PlatformEventCallback(void* context,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
|
||||
#define MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
|
||||
|
||||
@@ -38,6 +39,10 @@ struct InputCapabilityConfig {
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
bool keyboard_enabled = true;
|
||||
// Mouse look deltas keep their host units instead of being scaled into guest
|
||||
// surface units. The two differ only when the desktop display scale and the
|
||||
// window's pixel density disagree, as under fractional scaling.
|
||||
bool raw_mouse = false;
|
||||
};
|
||||
|
||||
struct NetworkProxyConfig {
|
||||
@@ -46,16 +51,29 @@ struct NetworkProxyConfig {
|
||||
int port = 0;
|
||||
};
|
||||
|
||||
// Text and image fields expand {place_name}, {place_icon}, and
|
||||
// {creator_name}. A field that expands an empty placeholder, or renders
|
||||
// empty, is left out of the activity.
|
||||
struct DiscordRpcTextConfig {
|
||||
std::string title = "Roblox";
|
||||
std::string browsing = "Browsing experiences";
|
||||
std::string joining = "Joining an experience";
|
||||
std::string playing = "{place_name}";
|
||||
std::string state = "Playing Roblox";
|
||||
std::string playing = "Playing {place_name}";
|
||||
std::string state = "by {creator_name}";
|
||||
std::string unknown_place = "Unknown experience";
|
||||
};
|
||||
|
||||
// Images are an https URL or a Discord application asset key. Discord shows
|
||||
// the small image only next to a large one.
|
||||
struct DiscordRpcImagesConfig {
|
||||
std::string large = "{place_icon}";
|
||||
std::string large_text = "{place_name}";
|
||||
std::string small = "roblox_small";
|
||||
std::string small_text = "Roblox";
|
||||
};
|
||||
|
||||
struct DiscordRpcConfig {
|
||||
bool enabled = false;
|
||||
bool enabled = true;
|
||||
bool show_place_name = true;
|
||||
bool show_elapsed_time = true;
|
||||
bool join_enabled = true;
|
||||
@@ -63,6 +81,7 @@ struct DiscordRpcConfig {
|
||||
std::string join_button_label = "Join Server";
|
||||
std::string application_id;
|
||||
DiscordRpcTextConfig text;
|
||||
DiscordRpcImagesConfig images;
|
||||
};
|
||||
|
||||
std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Local git hooks. Install once with: lefthook install
|
||||
# Mirrors the "Repository contracts" job in .github/workflows/ci.yml.
|
||||
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
shell-syntax:
|
||||
glob: "{scripts,tests,packaging}/**/*.sh"
|
||||
run: for f in {staged_files}; do bash -n "$f" || exit 1; done
|
||||
json:
|
||||
glob: "{config,packaging}/**/*.json"
|
||||
run: for f in {staged_files}; do jq empty "$f" || exit 1; done
|
||||
desktop-entry:
|
||||
glob: "*.desktop"
|
||||
run: desktop-file-validate {staged_files}
|
||||
|
||||
pre-push:
|
||||
commands:
|
||||
packaging-contracts:
|
||||
run: >-
|
||||
bash tests/managed_payload_build_contract_test.sh . &&
|
||||
bash tests/flatpak_packaging_test.sh . &&
|
||||
bash tests/native_packaging_test.sh .
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"app-id": "space.bigrat.mocktail",
|
||||
"app-id": "io.github.CoderDayton.nightcap",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "50",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See the top-level README "About this fork". -->
|
||||
# FreeBSD Linuxulator
|
||||
|
||||
FreeBSD Linuxulator support is experimental. I tested it on FreeBSD
|
||||
@@ -15,7 +16,7 @@ tested source revision is `aadd58dddcbc78f4d5594827b46b5633552b15ce`. Run
|
||||
the following commands as root.
|
||||
|
||||
```sh
|
||||
fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/komaruworld/mocktail/main/packaging/freebsd/linuxulator.patch
|
||||
fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/freebsd/linuxulator.patch
|
||||
cd /usr/src
|
||||
git apply /root/linuxulator.patch
|
||||
make -j2 kernel-toolchain
|
||||
@@ -40,11 +41,10 @@ After reboot, verify the result from the Fedora userspace. It must print
|
||||
chroot /compat/linux stat -f -c %t /proc
|
||||
```
|
||||
|
||||
## Run Mocktail
|
||||
## Run Nightcap
|
||||
|
||||
You can simply run the Mocktail AppImage from the
|
||||
[latest release](https://github.com/komaruworld/mocktail/releases/latest) or
|
||||
[nightly build](https://github.com/komaruworld/mocktail/releases/tag/continuous)
|
||||
You can simply run the Nightcap AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest)
|
||||
inside the Fedora userspace.
|
||||
|
||||
## Audio
|
||||
@@ -72,11 +72,11 @@ ctl.!default {
|
||||
EOF
|
||||
```
|
||||
|
||||
Launch Mocktail with the ALSA audio driver.
|
||||
Launch Nightcap with the ALSA audio driver.
|
||||
|
||||
```sh
|
||||
SDL_AUDIO_DRIVER=alsa ./Mocktail-x86_64.AppImage
|
||||
SDL_AUDIO_DRIVER=alsa ./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Mocktail cannot override this check because Roblox reads `/proc` directly. The
|
||||
Nightcap cannot override this check because Roblox reads `/proc` directly. The
|
||||
Linuxulator patch fixes the value before Roblox sees it.
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 617 B |
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 725 B |
|
Before Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 771 B |
|
Before Width: | Height: | Size: 1014 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,13 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Nightcap
|
||||
GenericName=Roblox Player
|
||||
Comment=Play Roblox on Linux
|
||||
Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u
|
||||
Icon=io.github.CoderDayton.nightcap
|
||||
StartupWMClass=io.github.CoderDayton.nightcap
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;
|
||||
X-Mocktail-Managed=true
|
||||
@@ -1,20 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Copyright 2026 Mocktail Project Authors -->
|
||||
<!-- SPDX-License-Identifier: CC0-1.0 -->
|
||||
<component type="desktop-application">
|
||||
<id>space.bigrat.mocktail</id>
|
||||
<name>Mocktail</name>
|
||||
<id>io.github.CoderDayton.nightcap</id>
|
||||
<name>Nightcap</name>
|
||||
<summary>Play Roblox on Linux</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<developer id="space.bigrat">
|
||||
<name>Mocktail Project Authors</name>
|
||||
<developer id="io.github.coderdayton">
|
||||
<name>vii</name>
|
||||
</developer>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#CEB3EF</color>
|
||||
<color type="primary" scheme_preference="dark">#2F2A6B</color>
|
||||
</branding>
|
||||
<url type="homepage">https://github.com/komaruworld/mocktail</url>
|
||||
<url type="homepage">https://github.com/CoderDayton/nightcap</url>
|
||||
<description>
|
||||
<p>
|
||||
Sign in to Roblox, browse experiences, and play them on Linux.
|
||||
@@ -38,7 +39,7 @@
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">space.bigrat.mocktail.desktop</launchable>
|
||||
<launchable type="desktop-id">io.github.CoderDayton.nightcap.desktop</launchable>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">moderate</content_attribute>
|
||||
<content_attribute id="violence-fantasy">moderate</content_attribute>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Copyright 2026 Mocktail Project Authors -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -6,7 +7,7 @@
|
||||
height="512"
|
||||
viewBox="96 96 320 320"
|
||||
fill="none">
|
||||
<title>Mocktail</title>
|
||||
<title>Nightcap</title>
|
||||
<mask id="mocktail-mark"
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
@@ -19,12 +20,12 @@
|
||||
</mask>
|
||||
<g mask="url(#mocktail-mark)">
|
||||
<path d="M461.343 267.246L461.165 265.65L444.708 108.145C444.345 105.047 443.009 102.143 440.894 99.8515C438.769 97.5865 436.001 96.028 432.962 95.3864C429.924 94.7448 426.761 95.051 423.903 96.2637C421.056 97.5364 418.67 99.6554 417.07 102.332L356.5 146L206 108.145L193.521 42.4325C193.5 39.3045 192.493 36.2626 190.644 33.7398C188.774 31.2601 186.189 29.4138 183.237 28.4502C180.285 27.4866 177.108 27.4521 174.135 28.3513C171.161 29.2864 168.555 31.1314 166.685 33.6262L73.5931 161.71L72.6616 163.009C59.2667 181.485 52.2413 203.814 52.6444 226.631C53.0476 249.448 60.8576 271.515 74.8967 289.505L75.0459 289.702L75.453 290.192L142.096 374.97L174.978 417.241L195.02 442.943C197.361 445.962 200.625 448.131 204.315 449.12C208.005 450.109 211.918 449.862 215.455 448.418L245.662 436.181L295.274 416.014L395.981 375.674L396.235 375.562C417.35 366.989 435.112 351.794 446.852 332.262C458.593 312.73 463.678 289.915 461.343 267.246Z"
|
||||
fill="#E24329"/>
|
||||
fill="#2F2A6B"/>
|
||||
<path d="M461.403 267.484L461.225 265.887C433.09 264.216 404.933 268.779 378.766 279.25L234.209 337.935C266.844 379.87 295.27 416.324 295.27 416.324L403.5 418L396.231 375.873C417.371 367.306 435.156 352.105 446.91 332.557C458.664 313.009 463.75 290.172 461.403 267.484Z"
|
||||
fill="#FC6D26"/>
|
||||
fill="#6C4FD1"/>
|
||||
<path d="M142.135 375.279L175.016 417.549L195.058 443.252C197.399 446.271 200.664 448.44 204.354 449.429C208.044 450.418 211.956 450.171 215.493 448.727L245.701 436.489L295.313 416.323C295.313 416.323 266.867 379.774 234.21 337.922C185.002 357.838 142.135 375.279 142.135 375.279Z"
|
||||
fill="#FCA326"/>
|
||||
fill="#A78BFA"/>
|
||||
<path d="M138.341 214.829C120.941 192.647 98.8514 174.589 73.6535 161.948L72.722 163.247C59.3272 181.723 52.3017 204.052 52.7049 226.869C53.1081 249.685 60.918 271.752 74.9572 289.743L75.1064 289.94L41.5 337.851L142.156 375.208C142.156 375.208 184.959 357.839 234.232 337.851L138.341 214.829Z"
|
||||
fill="#FC6D26"/>
|
||||
fill="#6C4FD1"/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Mocktail
|
||||
GenericName=Roblox Player
|
||||
Comment=Play Roblox on Linux
|
||||
Exec=mocktail %u
|
||||
Icon=space.bigrat.mocktail
|
||||
StartupWMClass=space.bigrat.mocktail
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;
|
||||
X-Mocktail-Managed=true
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -9,8 +10,8 @@ readonly REPOSITORY="${1:?Flatpak repository path is required}"
|
||||
readonly BUNDLE="${2:?Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${3:?GPG public key path is required}"
|
||||
readonly OUTPUT="${4:?Pages output path is required}"
|
||||
readonly NATIVE_REPOSITORIES="${5:?Native repositories path is required}"
|
||||
readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
readonly BASE_URL="https://coderdayton.github.io/nightcap"
|
||||
readonly APP_ID="io.github.CoderDayton.nightcap"
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
|
||||
@@ -26,13 +27,6 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -d "${NATIVE_REPOSITORIES}/apt" &&
|
||||
-d "${NATIVE_REPOSITORIES}/rpm" &&
|
||||
-d "${NATIVE_REPOSITORIES}/downloads" &&
|
||||
-s "${NATIVE_REPOSITORIES}/mocktail-packages.gpg" ]] || {
|
||||
printf 'Native repositories are missing: %s\n' "${NATIVE_REPOSITORIES}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ ! -e "${OUTPUT}" ]] || {
|
||||
printf 'Pages output already exists: %s\n' "${OUTPUT}" >&2
|
||||
exit 1
|
||||
@@ -40,50 +34,41 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
|
||||
mkdir -p -- "${OUTPUT}"
|
||||
cp -a -- "${REPOSITORY}" "${OUTPUT}/repo"
|
||||
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
|
||||
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Nightcap-x86_64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/nightcap-flatpak.gpg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/mocktail.svg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/discord-join.html" \
|
||||
"${OUTPUT}/join.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -d -- "${OUTPUT}/packaging"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/packaging/space.bigrat.mocktail.svg"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/apt" "${OUTPUT}/apt"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/rpm" "${OUTPUT}/rpm"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/downloads" "${OUTPUT}/downloads"
|
||||
install -m 0644 -- \
|
||||
"${NATIVE_REPOSITORIES}/mocktail-packages.gpg" \
|
||||
"${OUTPUT}/mocktail-packages.gpg"
|
||||
"${PROJECT_DIR}/packaging/${APP_ID}.svg" \
|
||||
"${OUTPUT}/nightcap.svg"
|
||||
touch -- "${OUTPUT}/.nojekyll"
|
||||
|
||||
readonly GPG_KEY="$(base64 --wrap=0 "${PUBLIC_KEY}")"
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakrepo" <<EOF
|
||||
cat >"${OUTPUT}/nightcap.flatpakrepo" <<EOF
|
||||
[Flatpak Repo]
|
||||
Title=Mocktail Nightly
|
||||
Title=Nightcap
|
||||
Url=${BASE_URL}/repo/
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Nightly builds of Mocktail
|
||||
Description=Signed x86_64 nightly builds published from the latest Mocktail main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
Homepage=https://github.com/CoderDayton/nightcap
|
||||
Comment=Nightcap releases
|
||||
Description=Signed x86_64 release builds of Nightcap
|
||||
Icon=${BASE_URL}/nightcap.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
EOF
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakref" <<EOF
|
||||
cat >"${OUTPUT}/nightcap.flatpakref" <<EOF
|
||||
[Flatpak Ref]
|
||||
Title=Mocktail Nightly
|
||||
Name=space.bigrat.mocktail
|
||||
Title=Nightcap
|
||||
Name=${APP_ID}
|
||||
Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Nightly build from the latest main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
Homepage=https://github.com/CoderDayton/nightcap
|
||||
Comment=Roblox on Linux, tuned for a real PC
|
||||
Icon=${BASE_URL}/nightcap.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
EOF
|
||||
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/terms.html" "${OUTPUT}/terms.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/privacy.html" "${OUTPUT}/privacy.html"
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -8,7 +9,7 @@ readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
||||
|
||||
BUILD_DIR="${PROJECT_ROOT}/build-flatpak"
|
||||
MANIFEST="${PROJECT_ROOT}/packaging/flatpak/space.bigrat.mocktail.json"
|
||||
MANIFEST="${PROJECT_ROOT}/packaging/flatpak/io.github.CoderDayton.nightcap.json"
|
||||
JOBS="${MOCKTAIL_FLATPAK_JOBS:-4}"
|
||||
|
||||
Usage() {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly project_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly source_icon="${project_root}/packaging/space.bigrat.mocktail.svg"
|
||||
readonly source_icon="${project_root}/packaging/io.github.CoderDayton.nightcap.svg"
|
||||
readonly output_root="${project_root}/packaging/icons/hicolor"
|
||||
readonly icon_name="space.bigrat.mocktail.png"
|
||||
readonly icon_name="io.github.CoderDayton.nightcap.png"
|
||||
readonly icon_sizes=(16 22 24 32 36 48 64 72 96 128 192 256 512)
|
||||
|
||||
if ! command -v magick >/dev/null 2>&1; then
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -152,8 +153,8 @@ AnyLinuxDeploy() {
|
||||
local -a deployment_environment=(
|
||||
"APPDIR=${app_dir}" "MAIN_BIN=mocktail" "LIB_DIR=/usr/lib"
|
||||
"TMPDIR=${ANYLINUX_WORK}/tmp"
|
||||
"DESKTOP=/usr/share/applications/space.bigrat.mocktail.desktop"
|
||||
"ICON=/usr/share/icons/hicolor/scalable/apps/space.bigrat.mocktail.svg"
|
||||
"DESKTOP=/usr/share/applications/io.github.CoderDayton.nightcap.desktop"
|
||||
"ICON=/usr/share/icons/hicolor/scalable/apps/io.github.CoderDayton.nightcap.svg"
|
||||
"DEPLOY_DATADIR=0" "DEPLOY_LOCALE=0" "DEPLOY_VULKAN=0"
|
||||
"STRACE_MODE=1" "STRACE_BINARY=mocktail" "STRACE_FLAGS=--help"
|
||||
"STRACE_TIME=1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -920,25 +921,25 @@ BuildAppImage() {
|
||||
ln -s "share/mocktail-bundle/run.sh" "${APPDIR}/usr/mocktail"
|
||||
install -m 0755 -- "${PROJECT_ROOT}/packaging/AppRun" "${APPDIR}/AppRun"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_ROOT}/packaging/space.bigrat.mocktail.desktop" \
|
||||
"${APPDIR}/space.bigrat.mocktail.desktop"
|
||||
"${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.desktop" \
|
||||
"${APPDIR}/io.github.CoderDayton.nightcap.desktop"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_ROOT}/packaging/space.bigrat.mocktail.desktop" \
|
||||
"${APPDIR}/usr/share/applications/space.bigrat.mocktail.desktop"
|
||||
"${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.desktop" \
|
||||
"${APPDIR}/usr/share/applications/io.github.CoderDayton.nightcap.desktop"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_ROOT}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${APPDIR}/space.bigrat.mocktail.svg"
|
||||
"${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.svg" \
|
||||
"${APPDIR}/io.github.CoderDayton.nightcap.svg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_ROOT}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${APPDIR}/usr/share/icons/hicolor/scalable/apps/space.bigrat.mocktail.svg"
|
||||
"${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.svg" \
|
||||
"${APPDIR}/usr/share/icons/hicolor/scalable/apps/io.github.CoderDayton.nightcap.svg"
|
||||
for icon_path in \
|
||||
"${PROJECT_ROOT}"/packaging/icons/hicolor/*x*/apps/space.bigrat.mocktail.png; do
|
||||
"${PROJECT_ROOT}"/packaging/icons/hicolor/*x*/apps/io.github.CoderDayton.nightcap.png; do
|
||||
[[ -f "${icon_path}" ]] || Die "hicolor icon set is incomplete"
|
||||
icon_relative_path="${icon_path#"${PROJECT_ROOT}/packaging/icons/hicolor/"}"
|
||||
install -D -m 0644 -- "${icon_path}" \
|
||||
"${APPDIR}/usr/share/icons/hicolor/${icon_relative_path}"
|
||||
done
|
||||
ln -s space.bigrat.mocktail.svg "${APPDIR}/.DirIcon"
|
||||
ln -s io.github.CoderDayton.nightcap.svg "${APPDIR}/.DirIcon"
|
||||
mkdir -p -- "$(dirname -- "${APPIMAGE_OUTPUT}")"
|
||||
ARCH=x86_64 appimagetool "${APPDIR}" "${APPIMAGE_OUTPUT}"
|
||||
rm -rf -- "${APPDIR}"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
readonly DESKTOP_ID="space.bigrat.mocktail.desktop"
|
||||
readonly DESKTOP_ID="io.github.CoderDayton.nightcap.desktop"
|
||||
readonly ROBLOX_SCHEME="x-scheme-handler/roblox"
|
||||
readonly ROBLOX_PLAYER_SCHEME="x-scheme-handler/roblox-player"
|
||||
|
||||
@@ -139,8 +140,9 @@ desktop_source="$(ResolveRegularFile "${desktop_source}" "desktop template")"
|
||||
|
||||
[[ "$(grep -Fxc '[Desktop Entry]' "${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'Type=Application' "${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'Icon=space.bigrat.mocktail' "${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'Exec=mocktail %u' "${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'Icon=io.github.CoderDayton.nightcap' "${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u' \
|
||||
"${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;' \
|
||||
"${desktop_source}")" == 1 &&
|
||||
"$(grep -Fxc 'X-Mocktail-Managed=true' "${desktop_source}")" == 1 &&
|
||||
@@ -173,7 +175,8 @@ temporary="$(mktemp "${applications_dir}/.${DESKTOP_ID}.XXXXXX.desktop")"
|
||||
trap 'rm -f -- "${temporary}"' EXIT
|
||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||
if [[ "${line}" == Exec=* ]]; then
|
||||
printf 'Exec=%s %%u\n' "${mocktail_executable}"
|
||||
printf 'Exec=env SDL_VIDEODRIVER=wayland,x11 %s %%u\n' \
|
||||
"${mocktail_executable}"
|
||||
if [[ -n "${launch_working_directory}" ]]; then
|
||||
printf 'Path=%s\n' "${launch_working_directory}"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Print the CHANGELOG.md section for one version, for use as GitHub release
|
||||
# notes. Usage: scripts/release_notes.sh v0.1.0 [CHANGELOG.md]
|
||||
# Exits 1 when the version has no section, so a release cannot ship without
|
||||
# notes.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly tag="${1:?tag is required, e.g. v0.1.0}"
|
||||
readonly changelog="${2:-$(dirname -- "${BASH_SOURCE[0]}")/../CHANGELOG.md}"
|
||||
readonly version="${tag#v}"
|
||||
|
||||
notes="$(awk -v version="${version}" '
|
||||
/^## \[/ {
|
||||
if (found) exit
|
||||
found = ($0 ~ "^## \\[" version "\\]")
|
||||
next
|
||||
}
|
||||
/^\[[^]]+\]: / { if (found) exit }
|
||||
found { print }
|
||||
' "${changelog}")"
|
||||
|
||||
# Trim leading and trailing blank lines.
|
||||
notes="$(printf '%s\n' "${notes}" | sed -e '/./,$!d' | sed -e ':a' -e '/^\n*$/{$d;N;ba' -e '}')"
|
||||
|
||||
[[ -n "${notes}" ]] || {
|
||||
printf 'CHANGELOG.md has no section for version %s\n' "${version}" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "${notes}"
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
"""Summarizes `--profile` traces as a Markdown table for pull requests.
|
||||
|
||||
Usage: summarize_profile_trace.py TRACE [TRACE ...]
|
||||
|
||||
Each trace gets its own column, so `before.json after.json` compares them.
|
||||
Frame times come from the "frame interval (ms)" counter; every other row is
|
||||
a slice name recorded by the Vulkan adapter.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Iterable
|
||||
|
||||
FRAME_COUNTER = "frame interval (ms)"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Distribution:
|
||||
count: int
|
||||
total: float
|
||||
mean: float
|
||||
p50: float
|
||||
p95: float
|
||||
p99: float
|
||||
max: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Summary:
|
||||
frames: Distribution | None
|
||||
slices: dict[str, Distribution]
|
||||
|
||||
|
||||
def _nearest_rank(ordered: list[float], percent: float) -> float:
|
||||
rank = max(1, math.ceil(percent / 100 * len(ordered)))
|
||||
return ordered[rank - 1]
|
||||
|
||||
|
||||
def _distribution(values: list[float]) -> Distribution:
|
||||
ordered = sorted(values)
|
||||
total = sum(ordered)
|
||||
return Distribution(count=len(ordered), total=total,
|
||||
mean=total / len(ordered),
|
||||
p50=_nearest_rank(ordered, 50),
|
||||
p95=_nearest_rank(ordered, 95),
|
||||
p99=_nearest_rank(ordered, 99), max=ordered[-1])
|
||||
|
||||
|
||||
def summarize(events: Iterable[dict]) -> Summary:
|
||||
frames: list[float] = []
|
||||
slices: dict[str, list[float]] = {}
|
||||
for event in events:
|
||||
phase = event.get("ph")
|
||||
if phase == "C" and event.get("name") == FRAME_COUNTER:
|
||||
frames.append(float(event["args"]["value"]))
|
||||
elif phase == "X":
|
||||
slices.setdefault(event["name"], []).append(event["dur"] / 1000)
|
||||
return Summary(
|
||||
frames=_distribution(frames) if frames else None,
|
||||
slices={name: _distribution(values)
|
||||
for name, values in slices.items()})
|
||||
|
||||
|
||||
def _row(label: str, cells: list[str]) -> str:
|
||||
return "| " + " | ".join([label, *cells]) + " |"
|
||||
|
||||
|
||||
def _cell(distribution: Distribution | None, field: str) -> str:
|
||||
if distribution is None:
|
||||
return "-"
|
||||
if field == "count":
|
||||
return str(distribution.count)
|
||||
return f"{getattr(distribution, field):.2f}"
|
||||
|
||||
|
||||
def _metric_row(label: str, distributions: list[Distribution | None],
|
||||
field: str) -> str:
|
||||
return _row(label, [_cell(item, field) for item in distributions])
|
||||
|
||||
|
||||
def to_markdown(traces: list[tuple[str, Summary]]) -> str:
|
||||
summaries = [summary for _, summary in traces]
|
||||
lines = [_row("metric", [name for name, _ in traces]),
|
||||
_row("---", ["---:"] * len(traces))]
|
||||
|
||||
frames = [summary.frames for summary in summaries]
|
||||
lines.append(_metric_row("frames", frames, "count"))
|
||||
if any(frames):
|
||||
lines += [_metric_row(f"frame interval {field} (ms)", frames, field)
|
||||
for field in ("mean", "p50", "p95", "p99", "max")]
|
||||
|
||||
# Largest total time first, taken from whichever trace spent the most.
|
||||
totals: dict[str, float] = {}
|
||||
for summary in summaries:
|
||||
for name, distribution in summary.slices.items():
|
||||
totals[name] = max(totals.get(name, 0.0), distribution.total)
|
||||
for name in sorted(totals, key=lambda name: -totals[name]):
|
||||
column = [summary.slices.get(name) for summary in summaries]
|
||||
lines.append(_metric_row(f"{name} count", column, "count"))
|
||||
lines += [_metric_row(f"{name} {field} (ms)", column, field)
|
||||
for field in ("total", "mean", "p99", "max")]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(arguments: list[str]) -> int:
|
||||
if not arguments:
|
||||
print(__doc__.strip(), file=sys.stderr)
|
||||
return 2
|
||||
traces = []
|
||||
for argument in arguments:
|
||||
path = Path(argument)
|
||||
traces.append((path.name, summarize(json.loads(path.read_text()))))
|
||||
print(to_markdown(traces))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -1,70 +1,51 @@
|
||||
<!doctype html>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Mocktail for Linux</title>
|
||||
<title>Nightcap for Linux</title>
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body><main>
|
||||
<img src="../packaging/space.bigrat.mocktail.svg" alt="Mocktail logo">
|
||||
<h1>Mocktail</h1>
|
||||
<p>Play Roblox on Linux. Use the stable Flathub release, or try the latest nightly build from main.</p>
|
||||
<a class="button" href="https://flathub.org/apps/space.bigrat.mocktail">Stable on Flathub</a>
|
||||
<a class="button secondary" href="https://mocktail.bigrat.space/mocktail.flatpakref">Nightly build</a>
|
||||
<img src="nightcap.svg" alt="Nightcap logo">
|
||||
<h1>Nightcap</h1>
|
||||
<p>Play Roblox on Linux like it was made for your PC. Nightcap is Mocktail, tuned to feel fast and look sharp.</p>
|
||||
<a class="button" href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage">Download AppImage</a>
|
||||
<a class="button secondary" href="nightcap.flatpakref">Install with Flatpak</a>
|
||||
<div class="commands">
|
||||
<p>Stable</p>
|
||||
<code>flatpak install flathub space.bigrat.mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
</div>
|
||||
<h2>AUR packages</h2>
|
||||
<p>Available from the AUR. Choose one package:</p>
|
||||
<p class="links">
|
||||
<a href="https://aur.archlinux.org/packages/mocktail">mocktail</a>
|
||||
<a href="https://aur.archlinux.org/packages/mocktail-bin">mocktail-bin</a>
|
||||
<a href="https://aur.archlinux.org/packages/mocktail-git">mocktail-git</a>
|
||||
</p>
|
||||
<code>yay -S mocktail
|
||||
yay -S mocktail-bin
|
||||
yay -S mocktail-git</code>
|
||||
<h2>DNF repository</h2>
|
||||
<p>Stable and nightly packages for Fedora 44. Choose one package.</p>
|
||||
<code>sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo -o /etc/yum.repos.d/mocktail.repo</code>
|
||||
<div class="commands">
|
||||
<p>Stable</p>
|
||||
<code>sudo dnf install mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>sudo dnf install mocktail-nightly</code>
|
||||
</div>
|
||||
<h2>APT repository</h2>
|
||||
<p>Stable and nightly packages for Ubuntu 26.04. Choose one package.</p>
|
||||
<code>sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg -o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update</code>
|
||||
<div class="commands">
|
||||
<p>Stable</p>
|
||||
<code>sudo apt install mocktail</code>
|
||||
<p>Nightly</p>
|
||||
<code>sudo apt install mocktail-nightly</code>
|
||||
<p>AppImage</p>
|
||||
<code>chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage</code>
|
||||
<p>Flatpak</p>
|
||||
<code>flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap</code>
|
||||
<p>Build from source</p>
|
||||
<code>git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap && make build
|
||||
./build/mocktail</code>
|
||||
</div>
|
||||
<h2>What's different</h2>
|
||||
<ul>
|
||||
<li>Lower input lag. Frames are shown as soon as they are ready.</li>
|
||||
<li>Sharper textures, with your own PNG overrides if you want them.</li>
|
||||
<li>ETC2 textures render correctly on desktop GPUs.</li>
|
||||
<li>No more error 319 kicks after joining a server.</li>
|
||||
<li>Roblox sees your real RAM and screen size.</li>
|
||||
</ul>
|
||||
<h2>Direct downloads</h2>
|
||||
<p>Prebuilt packages for Linux x86_64.</p>
|
||||
<p class="download-label">Stable</p>
|
||||
<p>Prebuilt builds for Linux x86_64.</p>
|
||||
<p class="links">
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/latest/download/Mocktail-x86_64.AppImage">AppImage</a>
|
||||
<a href="https://mocktail.bigrat.space/downloads/mocktail.deb">DEB</a>
|
||||
<a href="https://mocktail.bigrat.space/downloads/mocktail.rpm">RPM</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage">AppImage</a>
|
||||
<a href="Nightcap-x86_64.flatpak">Flatpak bundle</a>
|
||||
<a href="nightcap-flatpak.gpg">Signing key</a>
|
||||
<a href="terms.html">Terms</a>
|
||||
<a href="privacy.html">Privacy</a>
|
||||
</p>
|
||||
<p class="download-label">Nightly</p>
|
||||
<p class="links">
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.AppImage">AppImage</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.deb">DEB</a>
|
||||
<a href="https://github.com/komaruworld/mocktail/releases/download/continuous/mocktail-nightly.rpm">RPM</a>
|
||||
</p>
|
||||
<p><a class="source" href="https://github.com/komaruworld/mocktail">Source code</a></p>
|
||||
<p>Mocktail is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
<p><a class="source" href="https://github.com/CoderDayton/nightcap">Source code</a> ·
|
||||
<a class="source" href="https://github.com/komaruworld/mocktail">Upstream Mocktail</a></p>
|
||||
<p>Nightcap is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</main></body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Nightcap Privacy Policy</title>
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body><main>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Nightcap is a Linux runtime for the Roblox Android client. This policy covers Nightcap itself and its Discord application. Roblox and Discord have their own policies for the data they handle.</p>
|
||||
|
||||
<h2>What Nightcap collects</h2>
|
||||
<p>Nothing. Nightcap has no server. It does not collect, store, or send telemetry, analytics, crash reports, or account data to the Nightcap project.</p>
|
||||
|
||||
<h2>What stays on your machine</h2>
|
||||
<ul>
|
||||
<li>Configuration in <code>~/.config/mocktail/</code></li>
|
||||
<li>Session logs and support bundles in <code>~/.local/state/mocktail/</code></li>
|
||||
<li>Roblox login session under <code>~/.local/share/mocktail/</code></li>
|
||||
<li>Roblox cache, downloaded payloads, and shader cache under <code>~/.cache/mocktail/</code></li>
|
||||
</ul>
|
||||
<p>Flatpak installs keep the same layout under <code>~/.var/app/io.github.CoderDayton.nightcap/</code>.</p>
|
||||
|
||||
<h2>Discord Rich Presence</h2>
|
||||
<p>When Rich Presence is on, Nightcap sends the current experience name, its public icon URL, elapsed time, and an optional public server join code to the Discord desktop client on the same machine over a local socket. Discord displays this on your profile under its own privacy settings. Nightcap never signs in to Discord, never reads a Discord token, and never contacts Discord's servers.</p>
|
||||
<p>Set <code>integrations.discord_rpc.enabled: false</code> in <code>config.yaml</code> to turn it off.</p>
|
||||
|
||||
<h2>Roblox</h2>
|
||||
<p>Nightcap talks to Roblox's services exactly as the Roblox client would. That traffic is covered by the Roblox Privacy Policy.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Open an issue at <a class="source" href="https://github.com/CoderDayton/nightcap/issues">github.com/CoderDayton/nightcap/issues</a>.</p>
|
||||
|
||||
<p class="links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="terms.html">Terms of Service</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
|
||||
</p>
|
||||
</main></body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
/* Modified by vii from komaruworld/mocktail. See README "About this fork". */
|
||||
:root {
|
||||
font: 17px/1.55 system-ui, sans-serif;
|
||||
color-scheme: dark;
|
||||
@@ -5,8 +6,8 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0d1117;
|
||||
color: #e6edf3;
|
||||
background: #14122b;
|
||||
color: #ece8ff;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -30,8 +31,13 @@ h2 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #9da7b3;
|
||||
p,
|
||||
li {
|
||||
color: #b3aad6;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
a.button {
|
||||
@@ -39,14 +45,14 @@ a.button {
|
||||
margin: 16px 8px 16px 0;
|
||||
padding: 12px 18px;
|
||||
border-radius: 9px;
|
||||
background: #238636;
|
||||
background: #6c4fd1;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
background: #21262d;
|
||||
background: #2f2a6b;
|
||||
}
|
||||
|
||||
.commands {
|
||||
@@ -56,7 +62,7 @@ a.secondary {
|
||||
.commands p,
|
||||
.download-label {
|
||||
margin: 14px 0 5px;
|
||||
color: #e6edf3;
|
||||
color: #ece8ff;
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
@@ -67,9 +73,9 @@ code {
|
||||
display: block;
|
||||
padding: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
border: 1px solid #30363d;
|
||||
border: 1px solid #3a3470;
|
||||
border-radius: 6px;
|
||||
background: #161b22;
|
||||
background: #1d1a3d;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -80,9 +86,9 @@ code {
|
||||
.links a {
|
||||
display: inline-block;
|
||||
margin: 0 14px 6px 0;
|
||||
color: #e6edf3;
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
a.source {
|
||||
color: #e6edf3;
|
||||
color: #a78bfa;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Nightcap Terms of Service</title>
|
||||
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body><main>
|
||||
<h1>Terms of Service</h1>
|
||||
<p>Nightcap is free and open-source software licensed under the Apache License, Version 2.0. The license text shipped with the source governs your use of the software.</p>
|
||||
|
||||
<h2>No warranty</h2>
|
||||
<p>Nightcap is provided "as is", without warranty of any kind. The authors are not liable for any loss, damage, or account action that results from using it. See sections 7 and 8 of the Apache License.</p>
|
||||
|
||||
<h2>Not affiliated</h2>
|
||||
<p>Nightcap is an independent community project. It is not affiliated with, endorsed by, or supported by Roblox Corporation or Discord Inc. Roblox and Discord are trademarks of their respective owners.</p>
|
||||
|
||||
<h2>Your responsibility</h2>
|
||||
<p>You are responsible for following the Roblox Terms of Use and the Discord Terms of Service while using Nightcap with those services. Nightcap does not modify Roblox game logic, does not bypass Roblox security, and is not a cheat.</p>
|
||||
|
||||
<h2>Discord application</h2>
|
||||
<p>The Nightcap Discord application only provides Rich Presence for the Nightcap desktop runtime. It requests no permissions, has no bot, and does not access your Discord account.</p>
|
||||
|
||||
<h2>Changes</h2>
|
||||
<p>These terms may change with new releases. The current version always lives at this address.</p>
|
||||
|
||||
<p class="links">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
|
||||
</p>
|
||||
</main></body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
// Android Vulkan loader ABI -> host Vulkan loader + SDL3 WSI.
|
||||
|
||||
#include <dlfcn.h>
|
||||
@@ -16,7 +17,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
|
||||
#include "mocktail/graphics/chrome_trace_writer.h"
|
||||
#include "mocktail/graphics/present_mode_policy.h"
|
||||
#include "mocktail/graphics/vulkan_etc2_emulation.h"
|
||||
#include "mocktail/graphics/vulkan_text_overlay_compositor.h"
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
@@ -188,6 +191,7 @@ struct AdapterState {
|
||||
VkInstance latest_instance = VK_NULL_HANDLE;
|
||||
size_t active_instance_count = 0;
|
||||
mocktail::graphics::VulkanTextOverlayCompositor text_overlay;
|
||||
mocktail::graphics::VulkanEtc2Emulation etc2;
|
||||
std::atomic<NotePresentFn> note_present{nullptr};
|
||||
std::atomic<NoteHostPresentBeginFn> note_host_present_begin{nullptr};
|
||||
std::atomic<NoteHostPresentEndFn> note_host_present_end{nullptr};
|
||||
@@ -727,12 +731,92 @@ PFN_vkVoidFunction AdapterProc(const char* name) {
|
||||
MOCKTAIL_VK_PROC(vkQueueWaitIdle)
|
||||
MOCKTAIL_VK_PROC(vkDeviceWaitIdle)
|
||||
MOCKTAIL_VK_PROC(vkQueuePresentKHR)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFeatures2)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceFormatProperties2)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties)
|
||||
MOCKTAIL_VK_PROC(vkGetPhysicalDeviceImageFormatProperties2)
|
||||
MOCKTAIL_VK_PROC(vkCreateImage)
|
||||
MOCKTAIL_VK_PROC(vkDestroyImage)
|
||||
MOCKTAIL_VK_PROC(vkCreateImageView)
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory)
|
||||
MOCKTAIL_VK_PROC(vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_PROC(vkMapMemory)
|
||||
MOCKTAIL_VK_PROC(vkMapMemory2)
|
||||
MOCKTAIL_VK_PROC(vkUnmapMemory)
|
||||
MOCKTAIL_VK_PROC(vkUnmapMemory2)
|
||||
MOCKTAIL_VK_PROC(vkFreeMemory)
|
||||
MOCKTAIL_VK_PROC(vkDestroyBuffer)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyImage)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyImage2)
|
||||
MOCKTAIL_VK_PROC(vkCmdCopyBufferToImage2)
|
||||
MOCKTAIL_VK_PROC(vkCmdExecuteCommands)
|
||||
#undef MOCKTAIL_VK_PROC
|
||||
#define MOCKTAIL_VK_ALIAS(alias, function) \
|
||||
if (std::strcmp(name, #alias) == 0) { \
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(function); \
|
||||
}
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFeatures2KHR,
|
||||
vkGetPhysicalDeviceFeatures2)
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceFormatProperties2KHR,
|
||||
vkGetPhysicalDeviceFormatProperties2)
|
||||
MOCKTAIL_VK_ALIAS(vkGetPhysicalDeviceImageFormatProperties2KHR,
|
||||
vkGetPhysicalDeviceImageFormatProperties2)
|
||||
MOCKTAIL_VK_ALIAS(vkBindBufferMemory2KHR, vkBindBufferMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyImage2KHR, vkCmdCopyImage2)
|
||||
MOCKTAIL_VK_ALIAS(vkMapMemory2KHR, vkMapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkUnmapMemory2KHR, vkUnmapMemory2)
|
||||
MOCKTAIL_VK_ALIAS(vkCmdCopyBufferToImage2KHR, vkCmdCopyBufferToImage2)
|
||||
#undef MOCKTAIL_VK_ALIAS
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool NameInList(const char* name, const char* const* names,
|
||||
std::size_t count) {
|
||||
return name != nullptr &&
|
||||
std::any_of(names, names + count, [name](const char* candidate) {
|
||||
return std::strcmp(name, candidate) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Device commands wrapped for ETC2/EAC emulation.
|
||||
bool IsEtc2DeviceProc(const char* name) {
|
||||
static constexpr const char* kNames[] = {
|
||||
"vkCreateImage", "vkDestroyImage",
|
||||
"vkCreateImageView", "vkBindBufferMemory",
|
||||
"vkBindBufferMemory2", "vkBindBufferMemory2KHR",
|
||||
"vkMapMemory", "vkMapMemory2",
|
||||
"vkMapMemory2KHR", "vkUnmapMemory",
|
||||
"vkUnmapMemory2", "vkUnmapMemory2KHR",
|
||||
"vkFreeMemory", "vkDestroyBuffer",
|
||||
"vkCmdCopyBufferToImage", "vkCmdCopyBufferToImage2",
|
||||
"vkCmdCopyImage", "vkCmdCopyImage2",
|
||||
"vkCmdCopyImage2KHR",
|
||||
"vkCmdCopyBufferToImage2KHR", "vkCmdExecuteCommands",
|
||||
};
|
||||
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
|
||||
}
|
||||
|
||||
// Physical-device queries that report emulated ETC2/EAC support.
|
||||
bool IsEtc2PhysicalDeviceProc(const char* name) {
|
||||
static constexpr const char* kNames[] = {
|
||||
"vkGetPhysicalDeviceFeatures",
|
||||
"vkGetPhysicalDeviceFeatures2",
|
||||
"vkGetPhysicalDeviceFeatures2KHR",
|
||||
"vkGetPhysicalDeviceFormatProperties",
|
||||
"vkGetPhysicalDeviceFormatProperties2",
|
||||
"vkGetPhysicalDeviceFormatProperties2KHR",
|
||||
"vkGetPhysicalDeviceImageFormatProperties",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2KHR",
|
||||
};
|
||||
return NameInList(name, kNames, sizeof(kNames) / sizeof(kNames[0]));
|
||||
}
|
||||
|
||||
bool IsDeviceAdapterProc(const char* name) {
|
||||
return name != nullptr && (std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
return name != nullptr && (IsEtc2DeviceProc(name) || std::strcmp(name, "vkDestroyDevice") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue") == 0 ||
|
||||
std::strcmp(name, "vkGetDeviceQueue2") == 0 ||
|
||||
std::strcmp(name, "vkCreateSwapchainKHR") == 0 ||
|
||||
@@ -928,7 +1012,8 @@ VkResult WaitForSemaphoresObserved(const char* call_name,
|
||||
}
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
static_cast<void>(call_name);
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
call_name, "wait");
|
||||
const VkResult result = host_wait(device, wait_info, timeout);
|
||||
if (observation != nullptr) {
|
||||
observation->SetResult(result);
|
||||
@@ -959,11 +1044,24 @@ struct FpsWaitTrace {
|
||||
std::atomic<std::uint64_t> total_ns{0};
|
||||
std::atomic<std::uint64_t> max_ns{0};
|
||||
std::atomic<std::uint64_t> window_start_ns{0};
|
||||
// Longest time between consecutive call starts; for present, a frame time.
|
||||
std::atomic<std::uint64_t> max_gap_ns{0};
|
||||
std::atomic<std::uint64_t> last_start_ns{0};
|
||||
|
||||
void Record(std::uint64_t start_ns) {
|
||||
if (name == nullptr || start_ns == 0) {
|
||||
return;
|
||||
}
|
||||
const std::uint64_t previous_start =
|
||||
last_start_ns.exchange(start_ns, std::memory_order_relaxed);
|
||||
if (previous_start != 0 && start_ns > previous_start) {
|
||||
const std::uint64_t gap_ns = start_ns - previous_start;
|
||||
std::uint64_t max_gap = max_gap_ns.load(std::memory_order_relaxed);
|
||||
while (gap_ns > max_gap &&
|
||||
!max_gap_ns.compare_exchange_weak(max_gap, gap_ns,
|
||||
std::memory_order_relaxed)) {
|
||||
}
|
||||
}
|
||||
const std::uint64_t wait_ns = MonotonicNanos() - start_ns;
|
||||
total_ns.fetch_add(wait_ns, std::memory_order_relaxed);
|
||||
std::uint64_t max_wait = max_ns.load(std::memory_order_relaxed);
|
||||
@@ -984,14 +1082,18 @@ struct FpsWaitTrace {
|
||||
const std::uint64_t total = total_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t peak = max_ns.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t count = samples.exchange(0, std::memory_order_relaxed);
|
||||
const std::uint64_t gap_peak =
|
||||
max_gap_ns.exchange(0, std::memory_order_relaxed);
|
||||
window_start_ns.store(start_ns, std::memory_order_relaxed);
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
std::fprintf(stderr, " [fps] %s n=%llu avg=%llu us max=%llu us\n", name,
|
||||
static_cast<unsigned long long>(count),
|
||||
std::fprintf(stderr,
|
||||
" [fps] %s n=%llu avg=%llu us max=%llu us gap_max=%llu us\n",
|
||||
name, static_cast<unsigned long long>(count),
|
||||
static_cast<unsigned long long>(total / count / 1000ULL),
|
||||
static_cast<unsigned long long>(peak / 1000ULL));
|
||||
static_cast<unsigned long long>(peak / 1000ULL),
|
||||
static_cast<unsigned long long>(gap_peak / 1000ULL));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1030,7 +1132,12 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
}
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t present_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult result = host_present(queue, present_info);
|
||||
VkResult result = VK_SUCCESS;
|
||||
{
|
||||
mocktail::graphics::TraceScope scope(
|
||||
mocktail::graphics::ActiveProfileTrace(), "host present", "present");
|
||||
result = host_present(queue, present_info);
|
||||
}
|
||||
if (fps_trace) {
|
||||
PresentWaitTrace().Record(present_start_ns);
|
||||
}
|
||||
@@ -1042,6 +1149,171 @@ ObservedHostQueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Samples the time between successive adapter present calls.
|
||||
void RecordProfiledFrame(mocktail::graphics::ChromeTraceWriter* trace,
|
||||
std::uint64_t present_start_ns) {
|
||||
static std::atomic<std::uint64_t> previous_start_ns{0};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline and shader creation is wrapped only while profiling, so a normal
|
||||
// session calls the host driver directly.
|
||||
VkResult VKAPI_CALL ProfiledCreateGraphicsPipelines(
|
||||
VkDevice device, VkPipelineCache cache, std::uint32_t count,
|
||||
const VkGraphicsPipelineCreateInfo* infos,
|
||||
const VkAllocationCallbacks* allocator, VkPipeline* pipelines) {
|
||||
const auto host = reinterpret_cast<PFN_vkCreateGraphicsPipelines>(
|
||||
HostDeviceProc(device, "vkCreateGraphicsPipelines"));
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateGraphicsPipelines",
|
||||
"pipeline");
|
||||
const VkResult result =
|
||||
host(device, cache, count, infos, allocator, pipelines);
|
||||
scope.Arg("count", count);
|
||||
scope.Arg("cache", cache != VK_NULL_HANDLE);
|
||||
scope.Arg("result", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL ProfiledCreateComputePipelines(
|
||||
VkDevice device, VkPipelineCache cache, std::uint32_t count,
|
||||
const VkComputePipelineCreateInfo* infos,
|
||||
const VkAllocationCallbacks* allocator, VkPipeline* pipelines) {
|
||||
const auto host = reinterpret_cast<PFN_vkCreateComputePipelines>(
|
||||
HostDeviceProc(device, "vkCreateComputePipelines"));
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateComputePipelines",
|
||||
"pipeline");
|
||||
const VkResult result =
|
||||
host(device, cache, count, infos, allocator, pipelines);
|
||||
scope.Arg("count", count);
|
||||
scope.Arg("cache", cache != VK_NULL_HANDLE);
|
||||
scope.Arg("result", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL ProfiledCreateShaderModule(
|
||||
VkDevice device, const VkShaderModuleCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkShaderModule* shader_module) {
|
||||
const auto host = reinterpret_cast<PFN_vkCreateShaderModule>(
|
||||
HostDeviceProc(device, "vkCreateShaderModule"));
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreateShaderModule", "pipeline");
|
||||
const VkResult result = host(device, create_info, allocator, shader_module);
|
||||
scope.Arg("bytes", create_info != nullptr
|
||||
? static_cast<std::int64_t>(create_info->codeSize)
|
||||
: 0);
|
||||
scope.Arg("result", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult VKAPI_CALL ProfiledCreatePipelineCache(
|
||||
VkDevice device, const VkPipelineCacheCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkPipelineCache* cache) {
|
||||
const auto host = reinterpret_cast<PFN_vkCreatePipelineCache>(
|
||||
HostDeviceProc(device, "vkCreatePipelineCache"));
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkCreatePipelineCache", "pipeline");
|
||||
const VkResult result = host(device, create_info, allocator, cache);
|
||||
scope.Arg("initial_bytes",
|
||||
create_info != nullptr
|
||||
? static_cast<std::int64_t>(create_info->initialDataSize)
|
||||
: 0);
|
||||
scope.Arg("result", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Null unless profiling is on and `name` is a profiled device command.
|
||||
PFN_vkVoidFunction ProfileAdapterProc(const char* name) {
|
||||
if (name == nullptr || mocktail::graphics::ActiveProfileTrace() == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(
|
||||
ProfiledCreateGraphicsPipelines);
|
||||
}
|
||||
if (std::strcmp(name, "vkCreateComputePipelines") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(
|
||||
ProfiledCreateComputePipelines);
|
||||
}
|
||||
if (std::strcmp(name, "vkCreateShaderModule") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledCreateShaderModule);
|
||||
}
|
||||
if (std::strcmp(name, "vkCreatePipelineCache") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(ProfiledCreatePipelineCache);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
Function HostPhysicalDeviceProc(const char* name, const char* alias) {
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
instance = state.latest_instance;
|
||||
}
|
||||
PFN_vkVoidFunction proc = HostInstanceProc(instance, name);
|
||||
if (proc == nullptr && alias != nullptr) {
|
||||
proc = HostInstanceProc(instance, alias);
|
||||
}
|
||||
return reinterpret_cast<Function>(proc);
|
||||
}
|
||||
|
||||
bool Etc2EmulatedPhysicalDevice(VkPhysicalDevice physical_device) {
|
||||
return State().etc2.PhysicalDeviceNeedsEmulation(
|
||||
physical_device, HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
"vkGetPhysicalDeviceFeatures", nullptr));
|
||||
}
|
||||
|
||||
void ReleaseEtc2PoolCommandBuffers(VkDevice device,
|
||||
VkCommandPool command_pool) {
|
||||
std::vector<VkCommandBuffer> command_buffers;
|
||||
{
|
||||
AdapterState& state = State();
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
for (const auto& binding : state.command_buffer_bindings) {
|
||||
if (binding.device == device && binding.command_pool == command_pool) {
|
||||
command_buffers.push_back(binding.command_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (VkCommandBuffer command_buffer : command_buffers) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareEtc2Submit2(std::uint32_t submit_count,
|
||||
const VkSubmitInfo2* submits) {
|
||||
if (submits == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
for (std::uint32_t info = 0; info < submits[index].commandBufferInfoCount;
|
||||
++info) {
|
||||
State().etc2.PrepareSubmit(
|
||||
&submits[index].pCommandBufferInfos[info].commandBuffer, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -1245,9 +1517,13 @@ vkGetInstanceProcAddr(VkInstance instance, const char* name) {
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR)
|
||||
: nullptr;
|
||||
}
|
||||
if (const PFN_vkVoidFunction profiled = ProfileAdapterProc(name);
|
||||
profiled != nullptr) {
|
||||
return HostInstanceProc(instance, name) != nullptr ? profiled : nullptr;
|
||||
}
|
||||
if (const PFN_vkVoidFunction adapter = AdapterProc(name);
|
||||
adapter != nullptr) {
|
||||
if (IsDeviceAdapterProc(name) &&
|
||||
if ((IsDeviceAdapterProc(name) || IsEtc2PhysicalDeviceProc(name)) &&
|
||||
HostInstanceProc(instance, name) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1266,6 +1542,10 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device,
|
||||
const PFN_vkVoidFunction host = host_get_device_proc_addr != nullptr
|
||||
? host_get_device_proc_addr(device, name)
|
||||
: nullptr;
|
||||
if (const PFN_vkVoidFunction profiled = ProfileAdapterProc(name);
|
||||
profiled != nullptr) {
|
||||
return host != nullptr ? profiled : nullptr;
|
||||
}
|
||||
if (!IsDeviceAdapterProc(name)) {
|
||||
return host;
|
||||
}
|
||||
@@ -1306,6 +1586,31 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
|
||||
VkDeviceCreateInfo host_info = *create_info;
|
||||
EnabledDeviceFeatures enabled = InspectEnabledFeatures(*create_info);
|
||||
const bool etc2_emulated = State().etc2.PhysicalDeviceNeedsEmulation(
|
||||
physical_device, reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
HostInstanceProc(instance,
|
||||
"vkGetPhysicalDeviceFeatures")));
|
||||
VkPhysicalDeviceFeatures host_enabled_features{};
|
||||
VkPhysicalDeviceFeatures2* requested_features2 = nullptr;
|
||||
VkBool32 requested_features2_etc2 = VK_FALSE;
|
||||
if (etc2_emulated) {
|
||||
enabled.root.features.textureCompressionETC2 = VK_FALSE;
|
||||
if (create_info->pEnabledFeatures != nullptr) {
|
||||
host_enabled_features = *create_info->pEnabledFeatures;
|
||||
host_enabled_features.textureCompressionETC2 = VK_FALSE;
|
||||
host_info.pEnabledFeatures = &host_enabled_features;
|
||||
}
|
||||
// The feature chain is caller-owned; clear the emulated bit only for the
|
||||
// host call and restore it afterwards.
|
||||
requested_features2 = const_cast<VkPhysicalDeviceFeatures2*>(
|
||||
reinterpret_cast<const VkPhysicalDeviceFeatures2*>(FindFeature(
|
||||
create_info->pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)));
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2_etc2 =
|
||||
requested_features2->features.textureCompressionETC2;
|
||||
requested_features2->features.textureCompressionETC2 = VK_FALSE;
|
||||
}
|
||||
}
|
||||
enabled.root.pNext = &enabled.vulkan11;
|
||||
enabled.vulkan11.pNext = &enabled.vulkan12;
|
||||
enabled.vulkan12.pNext = nullptr;
|
||||
@@ -1341,11 +1646,25 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(
|
||||
|
||||
const VkResult result =
|
||||
host_create(physical_device, &host_info, allocator, device);
|
||||
if (requested_features2 != nullptr) {
|
||||
requested_features2->features.textureCompressionETC2 =
|
||||
requested_features2_etc2;
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
RegisterHostDeviceDispatch(*device, physical_device,
|
||||
host_get_device_proc_addr);
|
||||
VkPhysicalDeviceMemoryProperties memory_properties{};
|
||||
if (const auto host_memory_properties =
|
||||
reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
HostInstanceProc(instance,
|
||||
"vkGetPhysicalDeviceMemoryProperties"));
|
||||
host_memory_properties != nullptr) {
|
||||
host_memory_properties(physical_device, &memory_properties);
|
||||
}
|
||||
State().etc2.RegisterDevice(*device, physical_device, etc2_emulated,
|
||||
memory_properties, host_get_device_proc_addr);
|
||||
|
||||
std::uint32_t queue_family_count = 0;
|
||||
const auto host_get_queue_families =
|
||||
@@ -1403,10 +1722,15 @@ vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* allocator) {
|
||||
const auto host_destroy = reinterpret_cast<PFN_vkDestroyDevice>(
|
||||
HostDeviceProc(device, "vkDestroyDevice"));
|
||||
State().text_overlay.DestroyDevice(device);
|
||||
State().etc2.DestroyDevice(device);
|
||||
if (host_destroy != nullptr) {
|
||||
host_destroy(device, allocator);
|
||||
}
|
||||
RemoveHostDeviceDispatch(device);
|
||||
if (auto* trace = mocktail::graphics::ActiveProfileTrace();
|
||||
trace != nullptr) {
|
||||
trace->Flush();
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(VkDevice device,
|
||||
@@ -1562,6 +1886,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImageKHR(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkAcquireNextImageKHR", "wait");
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t acquire_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult host_result = host_acquire(
|
||||
@@ -1591,6 +1917,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImage2KHR(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkAcquireNextImage2KHR", "wait");
|
||||
const VkResult host_result =
|
||||
host_acquire(device, acquire_info, image_index);
|
||||
const VkResult result = NormalizeSwapchainResult(host_result);
|
||||
@@ -1615,6 +1943,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkWaitForFences", "wait");
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t fence_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult result =
|
||||
@@ -1650,6 +1980,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
ReleaseEtc2PoolCommandBuffers(device, command_pool);
|
||||
const VkResult result = host_reset(device, command_pool, flags);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1706,6 +2037,11 @@ VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
if (command_buffers != nullptr) {
|
||||
for (std::uint32_t index = 0; index < command_buffer_count; ++index) {
|
||||
State().etc2.ReleaseCommandBuffer(command_buffers[index]);
|
||||
}
|
||||
}
|
||||
host_free(device, command_pool, command_buffer_count, command_buffers);
|
||||
RemoveHostCommandBuffers(device, command_pool, command_buffer_count,
|
||||
command_buffers);
|
||||
@@ -1722,6 +2058,7 @@ VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return;
|
||||
}
|
||||
ReleaseEtc2PoolCommandBuffers(device, command_pool);
|
||||
host_destroy(device, command_pool, allocator);
|
||||
RemoveHostCommandPoolBindings(device, command_pool);
|
||||
observation.SetResult(VK_SUCCESS);
|
||||
@@ -1737,6 +2074,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
const VkResult result = host_begin(command_buffer, begin_info);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1765,6 +2103,7 @@ VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
State().etc2.ReleaseCommandBuffer(command_buffer);
|
||||
const VkResult result = host_reset(command_buffer, flags);
|
||||
observation.SetResult(result);
|
||||
return result;
|
||||
@@ -1801,6 +2140,15 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
if (submits != nullptr) {
|
||||
for (std::uint32_t index = 0; index < submit_count; ++index) {
|
||||
State().etc2.PrepareSubmit(submits[index].pCommandBuffers,
|
||||
submits[index].commandBufferCount);
|
||||
}
|
||||
}
|
||||
const VkResult result = State().text_overlay.QueueSubmit(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1817,6 +2165,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit2", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence, host_submit);
|
||||
observation.SetResult(result);
|
||||
@@ -1833,6 +2185,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueSubmit2KHR", "submit");
|
||||
scope.Arg("submits", submit_count);
|
||||
PrepareEtc2Submit2(submit_count, submits);
|
||||
const VkResult result = State().text_overlay.QueueSubmit2(
|
||||
queue, submit_count, submits, fence,
|
||||
reinterpret_cast<PFN_vkQueueSubmit2>(host_submit));
|
||||
@@ -1864,6 +2220,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) {
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkQueueWaitIdle", "wait");
|
||||
const bool fps_trace = FpsTraceEnabled();
|
||||
const std::uint64_t idle_start_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const VkResult result =
|
||||
@@ -1883,6 +2241,8 @@ VKAPI_ATTR VkResult VKAPI_CALL vkDeviceWaitIdle(VkDevice device) {
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::TraceScope scope(mocktail::graphics::ActiveProfileTrace(),
|
||||
"vkDeviceWaitIdle", "wait");
|
||||
const VkResult result =
|
||||
State().text_overlay.DeviceWaitIdle(device, host_wait);
|
||||
observation.SetResult(result);
|
||||
@@ -2001,8 +2361,15 @@ vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
observation.SetResult(VK_ERROR_INITIALIZATION_FAILED);
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::ChromeTraceWriter* const trace =
|
||||
mocktail::graphics::ActiveProfileTrace();
|
||||
mocktail::graphics::TraceScope scope(trace, "vkQueuePresentKHR", "present");
|
||||
if (trace != nullptr) {
|
||||
RecordProfiledFrame(trace, mocktail::graphics::TraceClockNanos());
|
||||
}
|
||||
const VkResult result = state.text_overlay.QueuePresent(
|
||||
queue, present_info, ObservedHostQueuePresent);
|
||||
scope.Arg("result", result);
|
||||
const VkResult normalized_result = NormalizeSwapchainResult(result);
|
||||
if (normalized_result == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
const NoteSurfaceOutOfDateFn note_surface_out_of_date =
|
||||
@@ -2025,4 +2392,250 @@ vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* present_info) {
|
||||
return normalized_result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures(
|
||||
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures* features) {
|
||||
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures>(
|
||||
"vkGetPhysicalDeviceFeatures", nullptr);
|
||||
if (host == nullptr || features == nullptr) {
|
||||
return;
|
||||
}
|
||||
host(physical_device, features);
|
||||
if (Etc2EmulatedPhysicalDevice(physical_device) &&
|
||||
mocktail::graphics::Etc2SupportAdvertised()) {
|
||||
features->textureCompressionETC2 = VK_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2(
|
||||
VkPhysicalDevice physical_device, VkPhysicalDeviceFeatures2* features) {
|
||||
const auto host = HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFeatures2>(
|
||||
"vkGetPhysicalDeviceFeatures2", "vkGetPhysicalDeviceFeatures2KHR");
|
||||
if (host == nullptr || features == nullptr) {
|
||||
return;
|
||||
}
|
||||
host(physical_device, features);
|
||||
if (Etc2EmulatedPhysicalDevice(physical_device) &&
|
||||
mocktail::graphics::Etc2SupportAdvertised()) {
|
||||
features->features.textureCompressionETC2 = VK_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties(
|
||||
VkPhysicalDevice physical_device, VkFormat format,
|
||||
VkFormatProperties* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties>(
|
||||
"vkGetPhysicalDeviceFormatProperties", nullptr);
|
||||
if (host == nullptr || properties == nullptr) {
|
||||
return;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
host(physical_device, format, properties);
|
||||
return;
|
||||
}
|
||||
host(physical_device, mapped.host_format, properties);
|
||||
properties->linearTilingFeatures = 0;
|
||||
properties->optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(
|
||||
properties->optimalTilingFeatures);
|
||||
properties->bufferFeatures = 0;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2(
|
||||
VkPhysicalDevice physical_device, VkFormat format,
|
||||
VkFormatProperties2* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceFormatProperties2>(
|
||||
"vkGetPhysicalDeviceFormatProperties2",
|
||||
"vkGetPhysicalDeviceFormatProperties2KHR");
|
||||
if (host == nullptr || properties == nullptr) {
|
||||
return;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
host(physical_device, format, properties);
|
||||
return;
|
||||
}
|
||||
host(physical_device, mapped.host_format, properties);
|
||||
VkFormatProperties& base = properties->formatProperties;
|
||||
base.linearTilingFeatures = 0;
|
||||
base.optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(base.optimalTilingFeatures);
|
||||
base.bufferFeatures = 0;
|
||||
if (auto* extended = const_cast<VkFormatProperties3*>(
|
||||
reinterpret_cast<const VkFormatProperties3*>(FindFeature(
|
||||
properties->pNext, VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3)));
|
||||
extended != nullptr) {
|
||||
extended->linearTilingFeatures = 0;
|
||||
extended->optimalTilingFeatures =
|
||||
mocktail::graphics::EmulatedEtc2FormatFeatures(
|
||||
static_cast<VkFormatFeatureFlags>(
|
||||
extended->optimalTilingFeatures));
|
||||
extended->bufferFeatures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties(
|
||||
VkPhysicalDevice physical_device, VkFormat format, VkImageType type,
|
||||
VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags,
|
||||
VkImageFormatProperties* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties>(
|
||||
"vkGetPhysicalDeviceImageFormatProperties", nullptr);
|
||||
if (host == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
return host(physical_device, format, type, tiling, usage, flags,
|
||||
properties);
|
||||
}
|
||||
if (tiling != VK_IMAGE_TILING_OPTIMAL || type != VK_IMAGE_TYPE_2D) {
|
||||
return VK_ERROR_FORMAT_NOT_SUPPORTED;
|
||||
}
|
||||
return host(physical_device, mapped.host_format, type, tiling, usage,
|
||||
flags & ~static_cast<VkImageCreateFlags>(
|
||||
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT),
|
||||
properties);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2(
|
||||
VkPhysicalDevice physical_device,
|
||||
const VkPhysicalDeviceImageFormatInfo2* info,
|
||||
VkImageFormatProperties2* properties) {
|
||||
const auto host =
|
||||
HostPhysicalDeviceProc<PFN_vkGetPhysicalDeviceImageFormatProperties2>(
|
||||
"vkGetPhysicalDeviceImageFormatProperties2",
|
||||
"vkGetPhysicalDeviceImageFormatProperties2KHR");
|
||||
if (host == nullptr || info == nullptr) {
|
||||
return VK_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
mocktail::graphics::Etc2EmulatedFormat mapped;
|
||||
if (!mocktail::graphics::LookupEmulatedEtc2Format(info->format, &mapped) ||
|
||||
!Etc2EmulatedPhysicalDevice(physical_device)) {
|
||||
return host(physical_device, info, properties);
|
||||
}
|
||||
if (info->tiling != VK_IMAGE_TILING_OPTIMAL ||
|
||||
info->type != VK_IMAGE_TYPE_2D) {
|
||||
return VK_ERROR_FORMAT_NOT_SUPPORTED;
|
||||
}
|
||||
VkPhysicalDeviceImageFormatInfo2 host_info = *info;
|
||||
host_info.format = mapped.host_format;
|
||||
host_info.flags &= ~static_cast<VkImageCreateFlags>(
|
||||
VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT);
|
||||
return host(physical_device, &host_info, properties);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
|
||||
VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image) {
|
||||
return State().etc2.CreateImage(device, create_info, allocator, image);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyImage(
|
||||
VkDevice device, VkImage image, const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.DestroyImage(device, image, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
|
||||
VkDevice device, const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImageView* view) {
|
||||
return State().etc2.CreateImageView(device, create_info, allocator, view);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device,
|
||||
VkBuffer buffer,
|
||||
VkDeviceMemory memory,
|
||||
VkDeviceSize offset) {
|
||||
return State().etc2.BindBufferMemory(device, buffer, memory, offset);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2(
|
||||
VkDevice device, std::uint32_t count,
|
||||
const VkBindBufferMemoryInfo* infos) {
|
||||
return State().etc2.BindBufferMemory2(device, count, infos);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device,
|
||||
VkDeviceMemory memory,
|
||||
VkDeviceSize offset,
|
||||
VkDeviceSize size,
|
||||
VkMemoryMapFlags flags,
|
||||
void** data) {
|
||||
return State().etc2.MapMemory(device, memory, offset, size, flags, data);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2(VkDevice device,
|
||||
const VkMemoryMapInfo* info,
|
||||
void** data) {
|
||||
return State().etc2.MapMemory2(device, info, data);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device,
|
||||
VkDeviceMemory memory) {
|
||||
State().etc2.UnmapMemory(device, memory);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2(VkDevice device,
|
||||
const VkMemoryUnmapInfo* info) {
|
||||
return State().etc2.UnmapMemory2(device, info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkFreeMemory(
|
||||
VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.FreeMemory(device, memory, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(
|
||||
VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) {
|
||||
State().etc2.DestroyBuffer(device, buffer, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(
|
||||
VkCommandBuffer command_buffer, VkBuffer source, VkImage destination,
|
||||
VkImageLayout layout, std::uint32_t region_count,
|
||||
const VkBufferImageCopy* regions) {
|
||||
State().etc2.CmdCopyBufferToImage(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
source, destination, layout, region_count, regions);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2(
|
||||
VkCommandBuffer command_buffer, const VkCopyBufferToImageInfo2* info) {
|
||||
State().etc2.CmdCopyBufferToImage2(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage(
|
||||
VkCommandBuffer command_buffer, VkImage source,
|
||||
VkImageLayout source_layout, VkImage destination,
|
||||
VkImageLayout destination_layout, std::uint32_t region_count,
|
||||
const VkImageCopy* regions) {
|
||||
State().etc2.CmdCopyImage(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
source, source_layout, destination, destination_layout, region_count,
|
||||
regions);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage2(VkCommandBuffer command_buffer,
|
||||
const VkCopyImageInfo2* info) {
|
||||
State().etc2.CmdCopyImage2(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
info);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL vkCmdExecuteCommands(
|
||||
VkCommandBuffer command_buffer, std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries) {
|
||||
State().etc2.CmdExecuteCommands(
|
||||
HostDispatchForCommandBuffer(command_buffer).device, command_buffer,
|
||||
count, secondaries);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,293 @@
|
||||
#include "mocktail/graphics/chrome_trace_writer.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
namespace {
|
||||
|
||||
// Buffered bytes that wake the writer before its periodic tick.
|
||||
constexpr std::size_t kWakeBytes = 256 * 1024;
|
||||
constexpr std::chrono::milliseconds kWriteInterval{250};
|
||||
// Every write ends the file with this, and the next write starts over it.
|
||||
constexpr char kArrayEnd[] = "\n]\n";
|
||||
constexpr long kArrayEndBytes = sizeof(kArrayEnd) - 1;
|
||||
|
||||
void AppendInteger(std::string* out, std::int64_t value) {
|
||||
char digits[24];
|
||||
const auto [end, error] =
|
||||
std::to_chars(digits, digits + sizeof(digits), value);
|
||||
if (error == std::errc()) {
|
||||
out->append(digits, end);
|
||||
} else {
|
||||
// Appending nothing would leave "ts": with no value, which costs the
|
||||
// whole trace rather than the one field.
|
||||
out->push_back('0');
|
||||
}
|
||||
}
|
||||
|
||||
void AppendDouble(std::string* out, double value) {
|
||||
// to_chars spells inf and nan without quotes, which no JSON reader accepts.
|
||||
if (!std::isfinite(value)) {
|
||||
out->push_back('0');
|
||||
return;
|
||||
}
|
||||
char digits[32];
|
||||
const auto [end, error] =
|
||||
std::to_chars(digits, digits + sizeof(digits), value);
|
||||
if (error == std::errc()) {
|
||||
out->append(digits, end);
|
||||
} else {
|
||||
out->push_back('0');
|
||||
}
|
||||
}
|
||||
|
||||
void AppendString(std::string* out, const char* value) {
|
||||
out->push_back('"');
|
||||
for (const char* cursor = value != nullptr ? value : ""; *cursor != '\0';
|
||||
++cursor) {
|
||||
const unsigned char byte = static_cast<unsigned char>(*cursor);
|
||||
if (byte == '"' || byte == '\\') {
|
||||
out->push_back('\\');
|
||||
out->push_back(static_cast<char>(byte));
|
||||
} else if (byte < 0x20) {
|
||||
static constexpr char kHex[] = "0123456789abcdef";
|
||||
out->append("\\u00");
|
||||
out->push_back(kHex[byte >> 4]);
|
||||
out->push_back(kHex[byte & 0xf]);
|
||||
} else {
|
||||
out->push_back(static_cast<char>(byte));
|
||||
}
|
||||
}
|
||||
out->push_back('"');
|
||||
}
|
||||
|
||||
void AppendHeader(std::string* out, const char* name, const char* category,
|
||||
char phase, std::uint64_t timestamp_ns) {
|
||||
static const std::int64_t pid = getpid();
|
||||
thread_local const std::int64_t tid = gettid();
|
||||
out->append("{\"name\":");
|
||||
AppendString(out, name);
|
||||
if (category != nullptr) {
|
||||
out->append(",\"cat\":");
|
||||
AppendString(out, category);
|
||||
}
|
||||
out->append(",\"ph\":\"");
|
||||
out->push_back(phase);
|
||||
out->append("\",\"ts\":");
|
||||
AppendInteger(out, static_cast<std::int64_t>(timestamp_ns / 1000));
|
||||
out->append(",\"pid\":");
|
||||
AppendInteger(out, pid);
|
||||
out->append(",\"tid\":");
|
||||
AppendInteger(out, tid);
|
||||
}
|
||||
|
||||
std::string& ThreadScratch() {
|
||||
thread_local std::string scratch;
|
||||
scratch.clear();
|
||||
return scratch;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::uint64_t TraceClockNanos() {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return static_cast<std::uint64_t>(now.tv_sec) * 1'000'000'000ULL +
|
||||
static_cast<std::uint64_t>(now.tv_nsec);
|
||||
}
|
||||
|
||||
std::unique_ptr<ChromeTraceWriter> ChromeTraceWriter::Open(
|
||||
const std::filesystem::path& path, std::string* error) {
|
||||
std::FILE* file = std::fopen(path.c_str(), "wb");
|
||||
if (file == nullptr || std::fputs("[", file) == EOF ||
|
||||
std::fputs(kArrayEnd, file) == EOF || std::fflush(file) != 0) {
|
||||
const int saved = errno;
|
||||
if (file != nullptr) {
|
||||
std::fclose(file);
|
||||
}
|
||||
if (error != nullptr) {
|
||||
*error = "could not open profile trace " + path.string() + ": " +
|
||||
std::strerror(saved);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return std::unique_ptr<ChromeTraceWriter>(new ChromeTraceWriter(file));
|
||||
}
|
||||
|
||||
ChromeTraceWriter::ChromeTraceWriter(std::FILE* file)
|
||||
: file_(file), owner_pid_(getpid()) {
|
||||
writer_ = std::thread([this] { WriterLoop(); });
|
||||
}
|
||||
|
||||
ChromeTraceWriter::~ChromeTraceWriter() { Close(); }
|
||||
|
||||
void ChromeTraceWriter::Slice(const char* name, const char* category,
|
||||
std::uint64_t start_ns, std::uint64_t end_ns,
|
||||
std::initializer_list<TraceArg> args) {
|
||||
Slice(name, category, start_ns, end_ns, args.begin(), args.size());
|
||||
}
|
||||
|
||||
void ChromeTraceWriter::Slice(const char* name, const char* category,
|
||||
std::uint64_t start_ns, std::uint64_t end_ns,
|
||||
const TraceArg* args, std::size_t arg_count) {
|
||||
std::string& event = ThreadScratch();
|
||||
AppendHeader(&event, name, category, 'X', start_ns);
|
||||
event.append(",\"dur\":");
|
||||
const std::uint64_t start_us = start_ns / 1000;
|
||||
const std::uint64_t end_us = end_ns / 1000;
|
||||
AppendInteger(&event, static_cast<std::int64_t>(
|
||||
end_us > start_us ? end_us - start_us : 0));
|
||||
if (args != nullptr && arg_count != 0) {
|
||||
event.append(",\"args\":{");
|
||||
for (std::size_t index = 0; index < arg_count; ++index) {
|
||||
if (index != 0) {
|
||||
event.push_back(',');
|
||||
}
|
||||
AppendString(&event, args[index].key);
|
||||
event.push_back(':');
|
||||
AppendInteger(&event, args[index].value);
|
||||
}
|
||||
event.push_back('}');
|
||||
}
|
||||
event.push_back('}');
|
||||
Append(event.data(), event.size());
|
||||
}
|
||||
|
||||
void ChromeTraceWriter::Counter(const char* name, std::uint64_t timestamp_ns,
|
||||
double value) {
|
||||
std::string& event = ThreadScratch();
|
||||
AppendHeader(&event, name, nullptr, 'C', timestamp_ns);
|
||||
event.append(",\"args\":{\"value\":");
|
||||
AppendDouble(&event, value);
|
||||
event.append("}}");
|
||||
Append(event.data(), event.size());
|
||||
}
|
||||
|
||||
void ChromeTraceWriter::Append(const char* event, std::size_t size) {
|
||||
bool wake = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (closed_) {
|
||||
return;
|
||||
}
|
||||
pending_.append(first_event_ ? "\n" : ",\n");
|
||||
first_event_ = false;
|
||||
pending_.append(event, size);
|
||||
wake = pending_.size() >= kWakeBytes;
|
||||
}
|
||||
if (wake) {
|
||||
wake_.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void ChromeTraceWriter::WriterLoop() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
while (!stopping_) {
|
||||
wake_.wait_for(lock, kWriteInterval, [this] {
|
||||
return stopping_ || pending_.size() >= kWakeBytes;
|
||||
});
|
||||
if (stopping_) {
|
||||
break;
|
||||
}
|
||||
lock.unlock();
|
||||
WritePending();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
// The file lock is taken before the buffer lock, so chunks reach the file in
|
||||
// the order they were buffered.
|
||||
void ChromeTraceWriter::WritePending() {
|
||||
std::lock_guard<std::mutex> file_lock(file_mutex_);
|
||||
spare_.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
pending_.swap(spare_);
|
||||
}
|
||||
if (file_ == nullptr || spare_.empty()) {
|
||||
return;
|
||||
}
|
||||
if (std::fseek(file_, -kArrayEndBytes, SEEK_END) != 0 ||
|
||||
std::fwrite(spare_.data(), 1, spare_.size(), file_) != spare_.size() ||
|
||||
std::fputs(kArrayEnd, file_) == EOF || std::fflush(file_) != 0) {
|
||||
std::fprintf(stderr, " [profile] trace write failed: %s\n",
|
||||
std::strerror(errno));
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
closed_ = true;
|
||||
// Nothing can reach the file now, so the writer thread exits instead of
|
||||
// ticking for the rest of the process.
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void ChromeTraceWriter::Flush() { WritePending(); }
|
||||
|
||||
void ChromeTraceWriter::Close() {
|
||||
// A forked child shares the file but not the writer thread.
|
||||
if (getpid() != owner_pid_) {
|
||||
return;
|
||||
}
|
||||
// A write failure sets stopping_ without joining the thread, so the join
|
||||
// below runs even when stopping_ is already set.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_all();
|
||||
if (writer_.joinable() && writer_.get_id() != std::this_thread::get_id()) {
|
||||
writer_.join();
|
||||
}
|
||||
WritePending();
|
||||
std::lock_guard<std::mutex> file_lock(file_mutex_);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
closed_ = true;
|
||||
}
|
||||
if (file_ != nullptr) {
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
ChromeTraceWriter* ActiveProfileTrace() {
|
||||
static ChromeTraceWriter* const trace = []() -> ChromeTraceWriter* {
|
||||
const char* path = std::getenv(kProfileTraceEnvironment);
|
||||
if (path == nullptr || path[0] == '\0') {
|
||||
return nullptr;
|
||||
}
|
||||
std::string error;
|
||||
std::unique_ptr<ChromeTraceWriter> writer =
|
||||
ChromeTraceWriter::Open(path, &error);
|
||||
if (writer == nullptr) {
|
||||
std::fprintf(stderr, " [profile] %s\n", error.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
std::fprintf(stderr, " [profile] writing Chrome trace to %s\n", path);
|
||||
// Leaked so threads still recording during exit never touch a destroyed
|
||||
// writer; they find it closed instead.
|
||||
static struct CloseAtExit {
|
||||
ChromeTraceWriter* writer;
|
||||
~CloseAtExit() { writer->Close(); }
|
||||
} close_at_exit{writer.get()};
|
||||
return writer.release();
|
||||
}();
|
||||
return trace;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
@@ -0,0 +1,493 @@
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<int, 8> kDistance = {3, 6, 11, 16, 23, 32, 41, 64};
|
||||
|
||||
// Indexed by (msb << 1) | lsb.
|
||||
constexpr int kEtc1Modifiers[8][4] = {
|
||||
{2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29},
|
||||
{13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80},
|
||||
{33, 106, -33, -106}, {47, 183, -47, -183},
|
||||
};
|
||||
|
||||
constexpr int kEacModifiers[16][8] = {
|
||||
{-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12},
|
||||
{-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12},
|
||||
{-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10},
|
||||
{-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10},
|
||||
{-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9},
|
||||
{-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9},
|
||||
{-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9},
|
||||
{-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8},
|
||||
};
|
||||
|
||||
std::uint8_t Clamp255(int value) {
|
||||
return static_cast<std::uint8_t>(std::clamp(value, 0, 255));
|
||||
}
|
||||
|
||||
int Expand4(int value) { return (value << 4) | value; }
|
||||
int Expand5(int value) { return (value << 3) | (value >> 2); }
|
||||
int Expand6(int value) { return (value << 2) | (value >> 4); }
|
||||
int Expand7(int value) { return (value << 1) | (value >> 6); }
|
||||
|
||||
// Decodes one ETC2 RGB block into 16 column-major RGBA texels (x * 4 + y).
|
||||
void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough,
|
||||
std::array<std::uint8_t, 64>* out) {
|
||||
static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1};
|
||||
const bool diff_or_opaque = (b[3] & 2) != 0;
|
||||
const bool opaque = !punchthrough || diff_or_opaque;
|
||||
const int msb = (b[4] << 8) | b[5];
|
||||
const int lsb = (b[6] << 8) | b[7];
|
||||
auto index_of = [msb, lsb](int texel) {
|
||||
return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1);
|
||||
};
|
||||
auto write = [out](int texel, int r, int g, int bl, int a) {
|
||||
const std::size_t o = static_cast<std::size_t>(texel) * 4;
|
||||
(*out)[o] = Clamp255(r);
|
||||
(*out)[o + 1] = Clamp255(g);
|
||||
(*out)[o + 2] = Clamp255(bl);
|
||||
(*out)[o + 3] = static_cast<std::uint8_t>(a);
|
||||
};
|
||||
|
||||
const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7];
|
||||
const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7];
|
||||
const int b_sum = (b[2] >> 3) + kDelta[b[2] & 7];
|
||||
const bool individual = !punchthrough && !diff_or_opaque;
|
||||
|
||||
if (!individual && (r_sum < 0 || r_sum > 31 || g_sum < 0 || g_sum > 31)) {
|
||||
std::array<std::array<int, 3>, 4> paint{};
|
||||
if (r_sum < 0 || r_sum > 31) {
|
||||
const std::array<int, 3> c1 = {
|
||||
Expand4((((b[0] >> 3) & 3) << 2) | (b[0] & 3)),
|
||||
Expand4(b[1] >> 4), Expand4(b[1] & 15)};
|
||||
const std::array<int, 3> c2 = {Expand4(b[2] >> 4), Expand4(b[2] & 15),
|
||||
Expand4(b[3] >> 4)};
|
||||
const int d = kDistance[(((b[3] >> 2) & 3) << 1) | (b[3] & 1)];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
paint[0][c] = c1[c];
|
||||
paint[1][c] = c2[c] + d;
|
||||
paint[2][c] = c2[c];
|
||||
paint[3][c] = c2[c] - d;
|
||||
}
|
||||
} else {
|
||||
const std::array<int, 3> c1 = {
|
||||
Expand4((b[0] >> 3) & 15),
|
||||
Expand4(((b[0] & 7) << 1) | ((b[1] >> 4) & 1)),
|
||||
Expand4((b[1] & 8) | ((b[1] & 3) << 1) | (b[2] >> 7))};
|
||||
const std::array<int, 3> c2 = {
|
||||
Expand4((b[2] >> 3) & 15), Expand4(((b[2] & 7) << 1) | (b[3] >> 7)),
|
||||
Expand4((b[3] >> 3) & 15)};
|
||||
const int v1 = (c1[0] << 16) | (c1[1] << 8) | c1[2];
|
||||
const int v2 = (c2[0] << 16) | (c2[1] << 8) | c2[2];
|
||||
const int d =
|
||||
kDistance[(b[3] & 4) | ((b[3] & 1) << 1) | (v1 >= v2 ? 1 : 0)];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
paint[0][c] = c1[c] + d;
|
||||
paint[1][c] = c1[c] - d;
|
||||
paint[2][c] = c2[c] + d;
|
||||
paint[3][c] = c2[c] - d;
|
||||
}
|
||||
}
|
||||
for (int texel = 0; texel < 16; ++texel) {
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
} else {
|
||||
write(texel, paint[index][0], paint[index][1], paint[index][2], 255);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!individual && (b_sum < 0 || b_sum > 31)) {
|
||||
const int ro = Expand6((b[0] >> 1) & 63);
|
||||
const int go = Expand7(((b[0] & 1) << 6) | ((b[1] >> 1) & 63));
|
||||
const int bo = Expand6(((b[1] & 1) << 5) | (b[2] & 0x18) |
|
||||
((b[2] & 3) << 1) | (b[3] >> 7));
|
||||
const int rh = Expand6(((b[3] & 0x7c) >> 1) | (b[3] & 1));
|
||||
const int gh = Expand7(b[4] >> 1);
|
||||
const int bh = Expand6(((b[4] & 1) << 5) | (b[5] >> 3));
|
||||
const int rv = Expand6(((b[5] & 7) << 3) | (b[6] >> 5));
|
||||
const int gv = Expand7(((b[6] & 31) << 2) | (b[7] >> 6));
|
||||
const int bv = Expand6(b[7] & 63);
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2,
|
||||
(x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2,
|
||||
(x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<std::array<int, 3>, 2> base{};
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
if (individual) {
|
||||
base[0][c] = Expand4(b[c] >> 4);
|
||||
base[1][c] = Expand4(b[c] & 15);
|
||||
} else {
|
||||
base[0][c] = Expand5(b[c] >> 3);
|
||||
base[1][c] = Expand5((b[c] >> 3) + kDelta[b[c] & 7]);
|
||||
}
|
||||
}
|
||||
const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7};
|
||||
const bool flipped = (b[3] & 1) != 0;
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
const int texel = x * 4 + y;
|
||||
const int index = index_of(texel);
|
||||
if (!opaque && index == 2) {
|
||||
write(texel, 0, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0);
|
||||
const int modifier =
|
||||
(!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index];
|
||||
write(texel, base[sub][0] + modifier, base[sub][1] + modifier,
|
||||
base[sub][2] + modifier, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EAC 3-bit index for column-major texel (x * 4 + y).
|
||||
int EacIndex(const std::uint8_t* b, int texel) {
|
||||
std::uint64_t bits = 0;
|
||||
for (int i = 2; i < 8; ++i) {
|
||||
bits = (bits << 8) | b[i];
|
||||
}
|
||||
return static_cast<int>((bits >> (45 - texel * 3)) & 7);
|
||||
}
|
||||
|
||||
int EacModifier(const std::uint8_t* b, int texel) {
|
||||
return kEacModifiers[b[1] & 15][EacIndex(b, texel)];
|
||||
}
|
||||
|
||||
std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) {
|
||||
const int multiplier = b[1] >> 4;
|
||||
const int modifier = EacModifier(b, texel);
|
||||
const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier;
|
||||
if (!is_signed) {
|
||||
const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047);
|
||||
return static_cast<std::uint16_t>((value << 5) | (value >> 6));
|
||||
}
|
||||
const int base = std::max(-127, static_cast<int>(static_cast<std::int8_t>(b[0])));
|
||||
const int value = std::clamp(base * 8 + scaled, -1023, 1023);
|
||||
const int magnitude = value < 0 ? -value : value;
|
||||
const int expanded = (magnitude << 5) | (magnitude >> 5);
|
||||
return static_cast<std::uint16_t>(
|
||||
static_cast<std::int16_t>(value < 0 ? -expanded : expanded));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t EtcBlockBytes(EtcFormat format) {
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed:
|
||||
return 16;
|
||||
default:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t EtcDecodedTexelBytes(EtcFormat format) {
|
||||
switch (format) {
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
return 2;
|
||||
default:
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Blocks per band handed to one worker: about a 128x128 texel image.
|
||||
constexpr std::uint64_t kBlocksPerBand = 4096;
|
||||
// Batches smaller than a 128x128 texel image decode on the caller. Waking a
|
||||
// pooled worker costs microseconds, so the floor only has to cover that.
|
||||
constexpr std::uint64_t kParallelMinimumBlocks = 4096;
|
||||
|
||||
bool ImageFits(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, const std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
return blocks_wide * blocks_high * EtcBlockBytes(format) <= source_bytes &&
|
||||
static_cast<std::uint64_t>(width) * height *
|
||||
EtcDecodedTexelBytes(format) <=
|
||||
destination_bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
return DecodeEtcImageBlockRows(format, source, source_bytes, width, height, 0,
|
||||
(height + 3) / 4, destination,
|
||||
destination_bytes);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct DecodeBand {
|
||||
EtcDecodeJob* job;
|
||||
std::uint32_t first_block_row;
|
||||
std::uint32_t block_row_count;
|
||||
};
|
||||
|
||||
struct DecodeBandQueue {
|
||||
std::vector<DecodeBand> bands;
|
||||
std::atomic<std::size_t> next{0};
|
||||
|
||||
void Run() {
|
||||
for (;;) {
|
||||
const std::size_t index = next.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index >= bands.size()) {
|
||||
return;
|
||||
}
|
||||
const DecodeBand& band = bands[index];
|
||||
const EtcDecodeJob& job = *band.job;
|
||||
DecodeEtcImageBlockRows(job.format, job.source, job.source_bytes,
|
||||
job.width, job.height, band.first_block_row,
|
||||
band.block_row_count, job.destination,
|
||||
job.destination_bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Decode workers outlive the batches that use them: creating and joining a
|
||||
// thread per batch costs more than decoding a small one. The threads park on
|
||||
// a condition variable and are never joined, so the pool is leaked at exit.
|
||||
class DecodePool {
|
||||
public:
|
||||
static DecodePool& Instance() {
|
||||
static DecodePool* pool = new DecodePool();
|
||||
return *pool;
|
||||
}
|
||||
|
||||
// Calls run(context) on the calling thread and on pooled workers, `workers`
|
||||
// calls in all. Returns false without calling it when another batch
|
||||
// already holds the pool or no worker could be started, leaving the whole
|
||||
// batch to the caller.
|
||||
bool Run(void (*run)(void*), void* context, std::size_t workers) {
|
||||
if (workers < 2) {
|
||||
return false;
|
||||
}
|
||||
std::unique_lock<std::mutex> batch(batch_mutex_, std::try_to_lock);
|
||||
if (!batch.owns_lock()) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t helpers = Reserve(workers - 1);
|
||||
if (helpers == 0) {
|
||||
return false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
run_ = run;
|
||||
context_ = context;
|
||||
unclaimed_ = helpers;
|
||||
running_ = helpers;
|
||||
}
|
||||
ready_.notify_all();
|
||||
run(context);
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
done_.wait(lock, [this] { return running_ == 0; });
|
||||
run_ = nullptr;
|
||||
context_ = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
DecodePool() = default;
|
||||
|
||||
// Grows the pool towards `wanted` and reports how many workers exist, so a
|
||||
// pthread_create failure degrades instead of deadlocking the wait below.
|
||||
std::size_t Reserve(std::size_t wanted) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
while (threads_ < wanted) {
|
||||
pthread_t worker;
|
||||
if (pthread_create(&worker, nullptr, &DecodePool::RunWorker, this) != 0) {
|
||||
break;
|
||||
}
|
||||
pthread_detach(worker);
|
||||
++threads_;
|
||||
}
|
||||
return std::min(threads_, wanted);
|
||||
}
|
||||
|
||||
static void* RunWorker(void* pool) {
|
||||
static_cast<DecodePool*>(pool)->WorkerLoop();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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();
|
||||
run(context);
|
||||
lock.lock();
|
||||
if (--running_ == 0) {
|
||||
done_.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex batch_mutex_;
|
||||
std::mutex 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::size_t threads_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count) {
|
||||
if (jobs == nullptr) {
|
||||
return;
|
||||
}
|
||||
DecodeBandQueue queue;
|
||||
std::vector<DecodeBand>& bands = queue.bands;
|
||||
std::uint64_t total_blocks = 0;
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
EtcDecodeJob& job = jobs[index];
|
||||
job.ok = ImageFits(job.format, job.source, job.source_bytes, job.width,
|
||||
job.height, job.destination, job.destination_bytes);
|
||||
if (!job.ok) {
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t blocks_wide = (job.width + 3) / 4;
|
||||
const std::uint32_t blocks_high = (job.height + 3) / 4;
|
||||
const std::uint32_t rows_per_band = static_cast<std::uint32_t>(
|
||||
std::max<std::uint64_t>(1, kBlocksPerBand / blocks_wide));
|
||||
for (std::uint32_t row = 0; row < blocks_high; row += rows_per_band) {
|
||||
bands.push_back(
|
||||
{&job, row, std::min(rows_per_band, blocks_high - row)});
|
||||
}
|
||||
total_blocks += static_cast<std::uint64_t>(blocks_wide) * blocks_high;
|
||||
}
|
||||
|
||||
const std::size_t threads =
|
||||
total_blocks < kParallelMinimumBlocks
|
||||
? 1
|
||||
: std::min<std::size_t>(std::max(worker_count, 1U), bands.size());
|
||||
RunOnDecodeWorkers(
|
||||
[](void* context) { static_cast<DecodeBandQueue*>(context)->Run(); },
|
||||
&queue, static_cast<unsigned>(threads));
|
||||
}
|
||||
|
||||
void RunOnDecodeWorkers(void (*run)(void*), void* context,
|
||||
unsigned worker_count) {
|
||||
if (run == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The pool declines when it is already busy; the caller drains the whole
|
||||
// batch itself then, exactly as a single-threaded batch does.
|
||||
if (!DecodePool::Instance().Run(run, context, worker_count)) {
|
||||
run(context);
|
||||
}
|
||||
}
|
||||
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes) {
|
||||
if (!ImageFits(format, source, source_bytes, width, height, destination,
|
||||
destination_bytes)) {
|
||||
return false;
|
||||
}
|
||||
const std::uint64_t blocks_wide = (static_cast<std::uint64_t>(width) + 3) / 4;
|
||||
const std::uint64_t blocks_high =
|
||||
(static_cast<std::uint64_t>(height) + 3) / 4;
|
||||
if (static_cast<std::uint64_t>(first_block_row) + block_row_count >
|
||||
blocks_high) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t texel_bytes = EtcDecodedTexelBytes(format);
|
||||
const std::size_t block_bytes = EtcBlockBytes(format);
|
||||
|
||||
std::array<std::uint8_t, 64> rgba{};
|
||||
const std::uint64_t end_block_row =
|
||||
static_cast<std::uint64_t>(first_block_row) + block_row_count;
|
||||
for (std::uint64_t by = first_block_row; by < end_block_row; ++by) {
|
||||
for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) {
|
||||
const std::uint8_t* block =
|
||||
source + (by * blocks_wide + bx) * block_bytes;
|
||||
if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) {
|
||||
DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba);
|
||||
} else if (format == EtcFormat::kEtc2Rgba8) {
|
||||
DecodeRgbBlock(block + 8, false, &rgba);
|
||||
}
|
||||
for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) {
|
||||
for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) {
|
||||
const int texel = static_cast<int>(x * 4 + y);
|
||||
std::uint8_t* out =
|
||||
destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes;
|
||||
switch (format) {
|
||||
case EtcFormat::kEtc2Rgb8:
|
||||
case EtcFormat::kEtc2Rgb8A1:
|
||||
case EtcFormat::kEtc2Rgba8:
|
||||
std::copy_n(rgba.data() + texel * 4, 4, out);
|
||||
if (format == EtcFormat::kEtc2Rgba8) {
|
||||
const int alpha =
|
||||
block[0] + EacModifier(block, texel) * (block[1] >> 4);
|
||||
out[3] = Clamp255(alpha);
|
||||
}
|
||||
break;
|
||||
case EtcFormat::kEacR11:
|
||||
case EtcFormat::kEacR11Signed:
|
||||
case EtcFormat::kEacRg11:
|
||||
case EtcFormat::kEacRg11Signed: {
|
||||
const bool is_signed = format == EtcFormat::kEacR11Signed ||
|
||||
format == EtcFormat::kEacRg11Signed;
|
||||
const int channels = texel_bytes / 2;
|
||||
for (int channel = 0; channel < channels; ++channel) {
|
||||
const std::uint16_t value =
|
||||
DecodeR11(block + channel * 8, texel, is_signed);
|
||||
out[channel * 2] = static_cast<std::uint8_t>(value);
|
||||
out[channel * 2 + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include "mocktail/graphics/present_mode_policy.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -28,10 +29,9 @@ PresentModePolicy ResolvePresentModePolicy(std::string_view vsync_value,
|
||||
if (frame_rate_value == "unlimited") {
|
||||
return PresentModePolicy::kUnthrottled;
|
||||
}
|
||||
if (frame_rate_value.empty() || frame_rate_value == "-1") {
|
||||
return PresentModePolicy::kHostDefault;
|
||||
}
|
||||
// Display or any fixed scheduler target keeps synchronized presentation.
|
||||
// Every synchronized target, including the unmanaged Roblox cap, presents
|
||||
// through the lowest-latency vsync mode the host offers. Roblox's own FIFO
|
||||
// request queues up to two rendered frames before they reach the display.
|
||||
return PresentModePolicy::kVsync;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#include "mocktail/graphics/texture_override.h"
|
||||
|
||||
#include <png.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size) {
|
||||
std::uint64_t hash = 0xcbf29ce484222325ULL;
|
||||
for (std::size_t index = 0; index < size; ++index) {
|
||||
hash ^= data[index];
|
||||
hash *= 0x100000001b3ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::string HashName(std::uint64_t hash) {
|
||||
char text[17];
|
||||
std::snprintf(text, sizeof(text), "%016llx",
|
||||
static_cast<unsigned long long>(hash));
|
||||
return text;
|
||||
}
|
||||
|
||||
bool ReadPngRgba(const std::string& path, RgbaImage* image) {
|
||||
if (image == nullptr) {
|
||||
return false;
|
||||
}
|
||||
png_image png{};
|
||||
png.version = PNG_IMAGE_VERSION;
|
||||
if (png_image_begin_read_from_file(&png, path.c_str()) == 0) {
|
||||
return false;
|
||||
}
|
||||
png.format = PNG_FORMAT_RGBA;
|
||||
RgbaImage result;
|
||||
result.width = png.width;
|
||||
result.height = png.height;
|
||||
result.pixels.resize(PNG_IMAGE_SIZE(png));
|
||||
if (png_image_finish_read(&png, nullptr, result.pixels.data(), 0,
|
||||
nullptr) == 0) {
|
||||
png_image_free(&png);
|
||||
return false;
|
||||
}
|
||||
*image = std::move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WritePngRgba(const std::string& path, const RgbaImage& image) {
|
||||
if (image.width == 0 || image.height == 0 ||
|
||||
image.pixels.size() !=
|
||||
static_cast<std::size_t>(image.width) * image.height * 4) {
|
||||
return false;
|
||||
}
|
||||
png_image png{};
|
||||
png.version = PNG_IMAGE_VERSION;
|
||||
png.width = image.width;
|
||||
png.height = image.height;
|
||||
png.format = PNG_FORMAT_RGBA;
|
||||
const int written = png_image_write_to_file(&png, path.c_str(), 0,
|
||||
image.pixels.data(), 0, nullptr);
|
||||
png_image_free(&png);
|
||||
return written != 0;
|
||||
}
|
||||
|
||||
void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination) {
|
||||
if (source.pixels.size() <
|
||||
static_cast<std::size_t>(source.width) * source.height * 4) {
|
||||
return;
|
||||
}
|
||||
ResampleRgba(source.pixels.data(), source.width, source.height, width,
|
||||
height, destination);
|
||||
}
|
||||
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination) {
|
||||
if (source == nullptr || destination == nullptr || width == 0 ||
|
||||
height == 0 || source_width == 0 || source_height == 0) {
|
||||
return;
|
||||
}
|
||||
// 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);
|
||||
bool one_texel_columns = true;
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const std::uint32_t x0 = static_cast<std::uint32_t>(
|
||||
static_cast<std::uint64_t>(x) * source_width / width);
|
||||
const std::uint32_t x1 = std::max<std::uint32_t>(
|
||||
x0 + 1, static_cast<std::uint32_t>(
|
||||
(static_cast<std::uint64_t>(x) + 1) * source_width / width));
|
||||
columns[2 * static_cast<std::size_t>(x)] = x0;
|
||||
columns[2 * static_cast<std::size_t>(x) + 1] = x1;
|
||||
one_texel_columns = one_texel_columns && x1 == x0 + 1;
|
||||
}
|
||||
const std::size_t row_bytes = static_cast<std::size_t>(width) * 4;
|
||||
std::uint64_t previous_y0 = 0;
|
||||
std::uint64_t previous_y1 = 0;
|
||||
for (std::uint32_t y = 0; y < height; ++y) {
|
||||
const std::uint64_t y0 = static_cast<std::uint64_t>(y) * source_height / height;
|
||||
std::uint64_t y1 =
|
||||
(static_cast<std::uint64_t>(y) + 1) * source_height / height;
|
||||
if (y1 <= y0) {
|
||||
y1 = y0 + 1;
|
||||
}
|
||||
std::uint8_t* out_row = destination + y * row_bytes;
|
||||
if (y > 0 && y0 == previous_y0 && y1 == previous_y1) {
|
||||
std::memcpy(out_row, out_row - row_bytes, row_bytes);
|
||||
continue;
|
||||
}
|
||||
previous_y0 = y0;
|
||||
previous_y1 = y1;
|
||||
if (one_texel_columns && y1 == y0 + 1) {
|
||||
const std::uint8_t* source_row = source + y0 * source_width * 4;
|
||||
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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (std::uint32_t x = 0; x < width; ++x) {
|
||||
const std::uint64_t x0 = columns[2 * static_cast<std::size_t>(x)];
|
||||
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) {
|
||||
for (std::uint64_t sx = x0; sx < x1; ++sx) {
|
||||
const std::uint8_t* pixel = source + (sy * source_width + sx) * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
sum[channel] += pixel[channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::uint64_t count = (y1 - y0) * (x1 - x0);
|
||||
std::uint8_t* out = destination + (static_cast<std::size_t>(y) * width + x) * 4;
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
out[channel] =
|
||||
static_cast<std::uint8_t>((sum[channel] + count / 2) / count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string EnvironmentString(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr ? value : "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextureOverrides::TextureOverrides(std::string override_dir,
|
||||
std::string dump_dir)
|
||||
: override_dir_(std::move(override_dir)), dump_dir_(std::move(dump_dir)) {}
|
||||
|
||||
TextureOverrides TextureOverrides::FromEnvironment() {
|
||||
return TextureOverrides(EnvironmentString("MOCKTAIL_TEXTURE_OVERRIDE_DIR"),
|
||||
EnvironmentString("MOCKTAIL_TEXTURE_DUMP_DIR"));
|
||||
}
|
||||
|
||||
std::shared_ptr<const RgbaImage> TextureOverrides::Lookup(std::uint64_t hash) {
|
||||
if (override_dir_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto cached = cache_.find(hash);
|
||||
if (cached != cache_.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
std::shared_ptr<const RgbaImage> loaded;
|
||||
RgbaImage image;
|
||||
const std::string path = override_dir_ + "/" + HashName(hash) + ".png";
|
||||
if (ReadPngRgba(path, &image) && image.width > 0 && image.height > 0) {
|
||||
loaded = std::make_shared<const RgbaImage>(std::move(image));
|
||||
std::fprintf(stderr, " [vulkan] texture override loaded: %s (%ux%u)\n",
|
||||
path.c_str(), loaded->width, loaded->height);
|
||||
}
|
||||
cache_[hash] = loaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void TextureOverrides::Dump(std::uint64_t hash, std::uint32_t width,
|
||||
std::uint32_t height, const std::uint8_t* rgba) {
|
||||
if (dump_dir_.empty() || rgba == nullptr || width == 0 || height == 0) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!dumped_.insert(hash).second) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(dump_dir_, error);
|
||||
RgbaImage image;
|
||||
image.width = width;
|
||||
image.height = height;
|
||||
image.pixels.assign(rgba, rgba + static_cast<std::size_t>(width) * height * 4);
|
||||
const std::string path = dump_dir_ + "/" + HashName(hash) + "_" +
|
||||
std::to_string(width) + "x" +
|
||||
std::to_string(height) + ".png";
|
||||
if (!WritePngRgba(path, image)) {
|
||||
std::fprintf(stderr, " [vulkan] texture dump failed: %s\n", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
#include "mocktail/audio/fmod_thread_floating_point.h"
|
||||
@@ -23,6 +24,8 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/display_size.h"
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
void Class::RegisterMethod(const std::string& method_name,
|
||||
@@ -59,7 +62,10 @@ thread_local JNIEnv* g_thread_local_env = nullptr;
|
||||
thread_local JNIEnv g_thread_env_storage = {};
|
||||
thread_local VM* g_thread_vm_instance = nullptr;
|
||||
thread_local mocktail::audio::FmodThreadFloatingPointMode g_thread_audio_fp_mode;
|
||||
thread_local std::vector<std::vector<jobject>> g_local_frames;
|
||||
// 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;
|
||||
// Authentication preflight can briefly own a second VM. Keep every live
|
||||
@@ -286,6 +292,10 @@ constexpr uint32_t kJniHandleShift = 16;
|
||||
std::shared_ptr<void> g_segment_owners[kJniSegmentCapacity];
|
||||
std::unordered_set<jclass> g_known_classes;
|
||||
std::unordered_map<std::string, jobject> g_singleton_objects;
|
||||
std::unordered_set<jobject> g_singleton_handles;
|
||||
// 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, std::shared_ptr<Class>> g_fallback_classes;
|
||||
std::unordered_set<jstring> g_known_strings;
|
||||
std::list<std::string> g_method_name_storage;
|
||||
@@ -336,7 +346,7 @@ void RegisterLocalRef(jobject obj) {
|
||||
return;
|
||||
}
|
||||
EnsureLocalFrame();
|
||||
g_local_frames.back().push_back(obj);
|
||||
++g_local_frames.back()[obj];
|
||||
}
|
||||
|
||||
void UnregisterLocalRef(jobject obj) {
|
||||
@@ -345,10 +355,27 @@ void UnregisterLocalRef(jobject obj) {
|
||||
}
|
||||
for (auto frame = g_local_frames.rbegin(); frame != g_local_frames.rend();
|
||||
++frame) {
|
||||
for (auto it = frame->rbegin(); it != frame->rend(); ++it) {
|
||||
if (*it == obj) {
|
||||
frame->erase(std::next(it).base());
|
||||
return;
|
||||
auto it = frame->find(obj);
|
||||
if (it != frame->end()) {
|
||||
if (--it->second == 0) {
|
||||
frame->erase(it);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseJniReference(jobject obj);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,6 +394,8 @@ void RetainJniReference(jobject obj) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -378,6 +407,7 @@ int AllocateSegmentSlot(void* value, std::shared_ptr<void> owner = nullptr,
|
||||
static_cast<uint32_t>(g_jni_ref_index) < kJniSegmentCapacity) {
|
||||
index = g_jni_ref_index++;
|
||||
} else {
|
||||
ReportHandleTableFull();
|
||||
return 0;
|
||||
}
|
||||
my_segment[index] = value;
|
||||
@@ -562,13 +592,25 @@ bool ResolvedDarkTheme() {
|
||||
}
|
||||
|
||||
jint IntResultForName(const char* name) {
|
||||
if (std::strcmp(name, "getScreenWidth") == 0 ||
|
||||
std::strcmp(name, "getWidth") == 0) {
|
||||
return 1280;
|
||||
if (std::strcmp(name, "getScreenWidth") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment))
|
||||
.width;
|
||||
}
|
||||
if (std::strcmp(name, "getScreenHeight") == 0 ||
|
||||
std::strcmp(name, "getHeight") == 0) {
|
||||
return 720;
|
||||
if (std::strcmp(name, "getScreenHeight") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment))
|
||||
.height;
|
||||
}
|
||||
if (std::strcmp(name, "getWidth") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment))
|
||||
.width;
|
||||
}
|
||||
if (std::strcmp(name, "getHeight") == 0) {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment))
|
||||
.height;
|
||||
}
|
||||
if (std::strcmp(name, "getDensityDpi") == 0) {
|
||||
return 160;
|
||||
@@ -724,6 +766,13 @@ std::shared_ptr<Class> ClassFromJClass(jclass clazz) {
|
||||
static jclass StoreClass(std::shared_ptr<Class> cls) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
Class* raw_ptr = cls.get();
|
||||
if (raw_ptr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto cached = g_class_handles.find(raw_ptr);
|
||||
if (cached != g_class_handles.end()) {
|
||||
return cached->second;
|
||||
}
|
||||
int index = AllocateSegmentSlot(raw_ptr, cls, SegmentType::kClass);
|
||||
if (index <= 0) {
|
||||
return nullptr;
|
||||
@@ -731,6 +780,7 @@ static jclass StoreClass(std::shared_ptr<Class> cls) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -775,10 +825,8 @@ void ReleaseJniReference(jobject obj) {
|
||||
g_known_classes.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& pair : g_singleton_objects) {
|
||||
if (pair.second == obj) {
|
||||
return;
|
||||
}
|
||||
if (g_singleton_handles.count(obj) != 0) {
|
||||
return;
|
||||
}
|
||||
const uint32_t index = JniIndexFromHandle(obj);
|
||||
if (index > 0 && index < kJniSegmentCapacity) {
|
||||
@@ -835,6 +883,7 @@ jobject SingletonObject(const std::string& class_name) {
|
||||
}
|
||||
jobject object = MakeObjectForClass(class_name);
|
||||
g_singleton_objects[class_name] = object;
|
||||
g_singleton_handles.insert(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -846,6 +895,38 @@ std::string_view ObjectClassName(jobject obj) {
|
||||
return pseudo_object->GetClass()->GetName();
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
jobject ExactMessageBusStaticObject(jclass clazz, jmethodID method_id) {
|
||||
const std::shared_ptr<Class> object_class = ClassFromJClass(clazz);
|
||||
if (object_class == nullptr ||
|
||||
@@ -866,6 +947,8 @@ jobject EngineJavaCallbackObject() {
|
||||
}
|
||||
g_engine_java_callback =
|
||||
MakeObjectForClass("com/roblox/engine/jni/EngineJavaCallback2");
|
||||
// VM-owned like a singleton, so detaching the creating thread keeps it.
|
||||
g_singleton_handles.insert(g_engine_java_callback);
|
||||
g_static_object_fields["sImplementation"] = g_engine_java_callback;
|
||||
return g_engine_java_callback;
|
||||
}
|
||||
@@ -1134,7 +1217,7 @@ static inline bool IsRawStringPointer(jstring str) {
|
||||
if (val < 0x10000) {
|
||||
return false;
|
||||
}
|
||||
if (val < 0x10000000ULL && (val & 0xffff) == 0) {
|
||||
if ((val & 0xffff) == 0 && (val >> kJniHandleShift) < kJniSegmentCapacity) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -2215,10 +2298,9 @@ bool HandleRobloxCookieSetVoidMethodA(jobject obj, jmethodID method_id,
|
||||
|
||||
jobject MakeFileObject(const char* path) {
|
||||
jobject file = MakeObjectForClass("java/io/File");
|
||||
auto* pseudo_object = PseudoObjectFromRef(file);
|
||||
if (pseudo_object) {
|
||||
pseudo_object->object_fields["path"] = MakeString(path);
|
||||
}
|
||||
// The field owns a reference, so the calling thread's local ref can be
|
||||
// released on detach without freeing the value.
|
||||
SetStringFieldRaw(file, "path", path);
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -2255,28 +2337,32 @@ jobject MakeDeviceStaticParamsObject() {
|
||||
auto* pseudo_object = PseudoObjectFromRef(object);
|
||||
if (pseudo_object) {
|
||||
const PlatformIdentity identity = CurrentPlatformIdentity();
|
||||
pseudo_object->object_fields["osVersion"] = MakeString("Android 13");
|
||||
pseudo_object->object_fields["deviceName"] =
|
||||
MakeString(identity.device_name.c_str());
|
||||
// The field owns a reference, so the calling thread's local ref can be
|
||||
// released on detach without freeing the value.
|
||||
const auto set_string = [pseudo_object](const char* field,
|
||||
const char* value) {
|
||||
jobject text = MakeString(value);
|
||||
RetainJniReference(text);
|
||||
pseudo_object->object_fields[field] = text;
|
||||
};
|
||||
set_string("osVersion", "Android 13");
|
||||
set_string("deviceName", identity.device_name.c_str());
|
||||
const char* app_version = std::getenv("MOCKTAIL_ROBLOX_VERSION");
|
||||
pseudo_object->object_fields["appVersion"] =
|
||||
MakeString(app_version != nullptr ? app_version : "unknown");
|
||||
pseudo_object->object_fields["manufacturer"] =
|
||||
MakeString(identity.manufacturer.c_str());
|
||||
pseudo_object->object_fields["model"] = MakeString(identity.model.c_str());
|
||||
pseudo_object->object_fields["brand"] = MakeString(identity.brand.c_str());
|
||||
pseudo_object->object_fields["device"] =
|
||||
MakeString(identity.device_code.c_str());
|
||||
pseudo_object->object_fields["deviceSku"] =
|
||||
MakeString(identity.device_sku.c_str());
|
||||
pseudo_object->object_fields["appBuildVariant"] = MakeString("headless");
|
||||
pseudo_object->object_fields["socModel"] =
|
||||
MakeString(identity.soc_model.c_str());
|
||||
pseudo_object->object_fields["soc_model"] =
|
||||
MakeString(identity.soc_model.c_str());
|
||||
set_string("appVersion", app_version != nullptr ? app_version : "unknown");
|
||||
set_string("manufacturer", identity.manufacturer.c_str());
|
||||
set_string("model", identity.model.c_str());
|
||||
set_string("brand", identity.brand.c_str());
|
||||
set_string("device", identity.device_code.c_str());
|
||||
set_string("deviceSku", identity.device_sku.c_str());
|
||||
set_string("appBuildVariant", "headless");
|
||||
set_string("socModel", identity.soc_model.c_str());
|
||||
set_string("soc_model", identity.soc_model.c_str());
|
||||
pseudo_object->boolean_fields["cpu64Bit"] = JNI_TRUE;
|
||||
pseudo_object->int_fields["screenWidth"] = 1280;
|
||||
pseudo_object->int_fields["screenHeight"] = 720;
|
||||
const mocktail::runtime::DisplaySize host_display =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
pseudo_object->int_fields["screenWidth"] = host_display.width;
|
||||
pseudo_object->int_fields["screenHeight"] = host_display.height;
|
||||
pseudo_object->int_fields["screenDensityDpi"] = 160;
|
||||
pseudo_object->int_fields["apiVersion"] = 33;
|
||||
pseudo_object->int_fields["sdkVersion"] = 33;
|
||||
@@ -2446,14 +2532,17 @@ void EnsureAndroidObjectGraph() {
|
||||
SetObjectFieldRaw(decor_view, "holder", surface_holder);
|
||||
SetObjectFieldRaw(decor_view, "surface", surface);
|
||||
SetObjectFieldRaw(decor_view, "display", display);
|
||||
SetIntFieldRaw(decor_view, "width", 1280);
|
||||
SetIntFieldRaw(decor_view, "height", 720);
|
||||
const mocktail::runtime::DisplaySize host_window =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
SetIntFieldRaw(decor_view, "width", host_window.width);
|
||||
SetIntFieldRaw(decor_view, "height", host_window.height);
|
||||
|
||||
SetObjectFieldRaw(surface_view, "holder", surface_holder);
|
||||
SetObjectFieldRaw(surface_view, "surface", surface);
|
||||
SetObjectFieldRaw(surface_view, "rootView", root_view);
|
||||
SetIntFieldRaw(surface_view, "width", 1280);
|
||||
SetIntFieldRaw(surface_view, "height", 720);
|
||||
SetIntFieldRaw(surface_view, "width", host_window.width);
|
||||
SetIntFieldRaw(surface_view, "height", host_window.height);
|
||||
|
||||
SetObjectFieldRaw(surface_holder, "surface", surface);
|
||||
SetObjectFieldRaw(surface_holder, "surfaceFrame",
|
||||
@@ -2461,11 +2550,14 @@ void EnsureAndroidObjectGraph() {
|
||||
SetBooleanFieldRaw(surface, "valid", JNI_TRUE);
|
||||
SetBooleanFieldRaw(surface, "isValid", JNI_TRUE);
|
||||
|
||||
SetIntFieldRaw(display, "width", 1280);
|
||||
SetIntFieldRaw(display, "height", 720);
|
||||
const mocktail::runtime::DisplaySize host_display =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
SetIntFieldRaw(display, "width", host_display.width);
|
||||
SetIntFieldRaw(display, "height", host_display.height);
|
||||
SetIntFieldRaw(display, "rotation", 0);
|
||||
SetIntFieldRaw(display_metrics, "widthPixels", 1280);
|
||||
SetIntFieldRaw(display_metrics, "heightPixels", 720);
|
||||
SetIntFieldRaw(display_metrics, "widthPixels", host_display.width);
|
||||
SetIntFieldRaw(display_metrics, "heightPixels", host_display.height);
|
||||
SetIntFieldRaw(display_metrics, "densityDpi", 160);
|
||||
SetFloatFieldRaw(display_metrics, "density", 1.0f);
|
||||
SetFloatFieldRaw(display_metrics, "scaledDensity", 1.0f);
|
||||
@@ -2476,9 +2568,11 @@ void EnsureAndroidObjectGraph() {
|
||||
: PlatformIdentity{};
|
||||
SetIntFieldRaw(configuration, "orientation", 2);
|
||||
SetIntFieldRaw(configuration, "densityDpi", 160);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", 1280);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", 720);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720);
|
||||
// Density is 1.0, so dp equals window pixels.
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", host_window.width);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", host_window.height);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp",
|
||||
std::min(host_window.width, host_window.height));
|
||||
SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1);
|
||||
SetIntFieldRaw(configuration, "keyboard", identity.keyboard_enabled ? 2 : 1);
|
||||
SetIntFieldRaw(configuration, "keyboardHidden",
|
||||
@@ -3839,8 +3933,30 @@ jobjectArray MakeObjectArray(jsize len, jobject init) {
|
||||
return ref;
|
||||
}
|
||||
|
||||
// NativeQuoteInterface.requestResponse([B)[B reply when no Android Keystore
|
||||
// exists: status byte 1, attestation-available byte 0, then the error text.
|
||||
// A real device with a keystore answers [1, 1, chain] instead.
|
||||
jbyteArray MakeQuoteUnavailableResponse() {
|
||||
static const char kMessage[] =
|
||||
"java.security.KeyStoreException: AndroidKeyStore is not available";
|
||||
const std::size_t message_length = sizeof(kMessage) - 1;
|
||||
jbyteArray result = MakeByteArray(static_cast<jsize>(2 + message_length));
|
||||
PseudoArray* array = ArrayFromRef(result);
|
||||
if (array == nullptr || array->bytes.size() != 2 + message_length) {
|
||||
return nullptr;
|
||||
}
|
||||
array->bytes[0] = 1;
|
||||
array->bytes[1] = 0;
|
||||
std::memcpy(array->bytes.data() + 2, kMessage, message_length);
|
||||
return result;
|
||||
}
|
||||
|
||||
jobject StaticObjectResultForMethod(jmethodID method_id) {
|
||||
const char* name = MethodName(method_id);
|
||||
if (std::strcmp(name, "requestResponse") == 0 &&
|
||||
std::strcmp(MethodSignature(method_id), "([B)[B") == 0) {
|
||||
return MakeQuoteUnavailableResponse();
|
||||
}
|
||||
const PlatformIdentity identity = CurrentPlatformIdentity();
|
||||
jobject android_object = AndroidObjectForMethod(name);
|
||||
if (android_object) {
|
||||
@@ -4952,10 +5068,15 @@ jobject CreateAndroidConfiguration(JNIEnv* env) {
|
||||
SetIntFieldRaw(configuration, "navigation", 1);
|
||||
SetIntFieldRaw(configuration, "navigationHidden", 1);
|
||||
SetIntFieldRaw(configuration, "orientation", 2);
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", 720);
|
||||
// Density is 1.0, so dp equals window pixels.
|
||||
const mocktail::runtime::DisplaySize config_window =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
SetIntFieldRaw(configuration, "screenHeightDp", config_window.height);
|
||||
SetIntFieldRaw(configuration, "screenLayout", 0);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", 1280);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp", 720);
|
||||
SetIntFieldRaw(configuration, "screenWidthDp", config_window.width);
|
||||
SetIntFieldRaw(configuration, "smallestScreenWidthDp",
|
||||
std::min(config_window.width, config_window.height));
|
||||
SetIntFieldRaw(configuration, "touchscreen", identity.touch_enabled ? 3 : 1);
|
||||
SetIntFieldRaw(configuration, "uiMode", 0);
|
||||
return configuration;
|
||||
@@ -6433,6 +6554,7 @@ void VM::InitJNIFunctionTables() {
|
||||
if (g_thread_vm_instance != owner || !IsThreadLocalEnvValid()) {
|
||||
return JNI_EDETACHED;
|
||||
}
|
||||
ReleaseThreadLocalRefs();
|
||||
g_thread_audio_fp_mode.Restore();
|
||||
g_thread_local_env = nullptr;
|
||||
g_thread_vm_instance = nullptr;
|
||||
@@ -6562,17 +6684,26 @@ void VM::InitJNIFunctionTables() {
|
||||
native_interface_.PopLocalFrame =
|
||||
[](JNIEnv* /*env*/, jobject result) -> jobject {
|
||||
EnsureLocalFrame();
|
||||
std::vector<jobject> frame = std::move(g_local_frames.back());
|
||||
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 (jobject obj : frame) {
|
||||
if (obj != result) {
|
||||
bool result_kept = false;
|
||||
for (const auto& [obj, count] : frame) {
|
||||
uint32_t releases = count;
|
||||
if (obj == result) {
|
||||
--releases;
|
||||
result_kept = true;
|
||||
}
|
||||
for (uint32_t i = 0; i < releases; ++i) {
|
||||
ReleaseJniReference(obj);
|
||||
}
|
||||
}
|
||||
if (result != nullptr && !result_kept) {
|
||||
RetainJniReference(result);
|
||||
}
|
||||
RegisterLocalRef(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -56,6 +58,10 @@
|
||||
#include "libc_shim/libc_shim.h"
|
||||
#include "linker/linker.h"
|
||||
#include "mocktail/graphics/bionic_egl_bridge.h"
|
||||
#include "mocktail/graphics/chrome_trace_writer.h"
|
||||
#include "runtime/engine_pump_rest.h"
|
||||
#include "runtime/device_memory_profile.h"
|
||||
#include "runtime/display_size.h"
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/discord_rpc.h"
|
||||
#include "runtime/jnivm_platform_web_callbacks.h"
|
||||
@@ -70,6 +76,7 @@
|
||||
#include "runtime/roblox_text_input_jni_bridge.h"
|
||||
#include "runtime/runtime_config.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
#include "runtime/texture_memory_policy.h"
|
||||
#include "services/client_settings_service.h"
|
||||
#include "services/http_client.h"
|
||||
#include "window/window.h"
|
||||
@@ -757,7 +764,12 @@ uint64_t MonotonicNanos() {
|
||||
static_cast<uint64_t>(ts.tv_nsec);
|
||||
}
|
||||
|
||||
JNIEnv* AttachMainThreadJniEnv() {
|
||||
// `attached` reports whether AttachCurrentThread itself succeeded, so a caller
|
||||
// that caches the env can tell a real attach from the GetJNIEnv fallback.
|
||||
JNIEnv* AttachMainThreadJniEnv(bool* attached = nullptr) {
|
||||
if (attached != nullptr) {
|
||||
*attached = false;
|
||||
}
|
||||
if (g_vm_for_main_thread_pump == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -768,6 +780,9 @@ JNIEnv* AttachMainThreadJniEnv() {
|
||||
jint attach_result = java_vm->AttachCurrentThread(&raw_env, nullptr);
|
||||
if (attach_result == JNI_OK && raw_env != nullptr) {
|
||||
env = static_cast<JNIEnv*>(raw_env);
|
||||
if (attached != nullptr) {
|
||||
*attached = true;
|
||||
}
|
||||
} else if (IsEnabled("MOCKTAIL_TRACE_MAIN_THREAD_PUMP")) {
|
||||
std::cerr << " [main] AttachCurrentThread failed in pump: "
|
||||
<< attach_result << '\n'
|
||||
@@ -782,6 +797,35 @@ JNIEnv* AttachMainThreadJniEnv() {
|
||||
return env;
|
||||
}
|
||||
|
||||
// The pseudo-VM attach is idempotent: once this thread holds a live env for
|
||||
// the VM, re-attaching hands back the same thread-local pointer. Only the
|
||||
// main thread calls this, so the cache needs no synchronization. The game
|
||||
// session installs RestoreGameSessionJniEnvironment, which restores the JNI
|
||||
// function tables when a guest JNI_OnLoad replaces them.
|
||||
JNIEnv* MainThreadPumpJniEnv() {
|
||||
static JNIEnv* cached_env = nullptr;
|
||||
static const jnivm::VM* cached_vm = nullptr;
|
||||
if (cached_env != nullptr && cached_vm == g_vm_for_main_thread_pump) {
|
||||
// Only the attach is cached. The table restore and the env publish run on
|
||||
// every tick: a guest JNI_OnLoad can replace the function tables, and any
|
||||
// other thread that publishes its own env overwrites the single global
|
||||
// slot this one shares.
|
||||
g_vm_for_main_thread_pump->RestoreFunctions();
|
||||
PublishCurrentJniEnv(cached_env);
|
||||
return cached_env;
|
||||
}
|
||||
// Only a real attach is worth caching. The GetJNIEnv fallback means this
|
||||
// thread is not attached to the VM, and the next tick has to retry: a VM
|
||||
// swap can make AttachCurrentThread fail for one tick and succeed after.
|
||||
bool attached = false;
|
||||
JNIEnv* env = AttachMainThreadJniEnv(&attached);
|
||||
if (env != nullptr && attached) {
|
||||
cached_env = env;
|
||||
cached_vm = g_vm_for_main_thread_pump;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
bool InvokeTaskSchedulerForeground(
|
||||
JNIEnv* env, jclass native_gl_class,
|
||||
NativeSetTaskSchedulerBackgroundModeFn native_set_task_scheduler_background_mode,
|
||||
@@ -900,6 +944,43 @@ bool RunTaskSchedulerForegroundOnMainThread(
|
||||
}
|
||||
}
|
||||
|
||||
// The profile trace writer is compiled into the Vulkan adapter, which the host
|
||||
// dlopens RTLD_GLOBAL, so the pump slice goes through symbols resolved from it.
|
||||
// Until the adapter is loaded there is no writer and nothing is recorded.
|
||||
struct PumpTraceHooks {
|
||||
using ActiveFn = mocktail::graphics::ChromeTraceWriter* (*)();
|
||||
using ClockFn = std::uint64_t (*)();
|
||||
using SliceFn = void (*)(mocktail::graphics::ChromeTraceWriter*, const char*,
|
||||
const char*, std::uint64_t, std::uint64_t,
|
||||
const mocktail::graphics::TraceArg*, std::size_t);
|
||||
ActiveFn active = nullptr;
|
||||
ClockFn clock = nullptr;
|
||||
SliceFn slice = nullptr;
|
||||
};
|
||||
|
||||
const PumpTraceHooks& PumpTrace() {
|
||||
static PumpTraceHooks hooks;
|
||||
// The adapter loads during startup, so a few hundred ticks cover it. After
|
||||
// that the lookups stop and the pump stays untraced.
|
||||
static int attempts = 0;
|
||||
if (hooks.active == nullptr && attempts++ < 1000) {
|
||||
PumpTraceHooks resolved;
|
||||
resolved.active = reinterpret_cast<PumpTraceHooks::ActiveFn>(
|
||||
::dlsym(RTLD_DEFAULT, "_ZN8mocktail8graphics18ActiveProfileTraceEv"));
|
||||
resolved.clock = reinterpret_cast<PumpTraceHooks::ClockFn>(
|
||||
::dlsym(RTLD_DEFAULT, "_ZN8mocktail8graphics15TraceClockNanosEv"));
|
||||
resolved.slice = reinterpret_cast<PumpTraceHooks::SliceFn>(::dlsym(
|
||||
RTLD_DEFAULT,
|
||||
"_ZN8mocktail8graphics17ChromeTraceWriter5SliceEPKcS3_mmPKNS0_"
|
||||
"8TraceArgEm"));
|
||||
if (resolved.active != nullptr && resolved.clock != nullptr &&
|
||||
resolved.slice != nullptr) {
|
||||
hooks = resolved;
|
||||
}
|
||||
}
|
||||
return hooks;
|
||||
}
|
||||
|
||||
void PumpRobloxMainThreadMessagesOnce() {
|
||||
static const bool force_early =
|
||||
IsEnabled("MOCKTAIL_FORCE_EARLY_MAIN_THREAD_MESSAGE_PUMP");
|
||||
@@ -939,7 +1020,7 @@ void PumpRobloxMainThreadMessagesOnce() {
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv* env = AttachMainThreadJniEnv();
|
||||
JNIEnv* env = MainThreadPumpJniEnv();
|
||||
if (env == nullptr) {
|
||||
static bool logged_missing_env = false;
|
||||
if (!logged_missing_env && trace_pump) {
|
||||
@@ -961,8 +1042,21 @@ void PumpRobloxMainThreadMessagesOnce() {
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
const PumpTraceHooks& hooks = PumpTrace();
|
||||
mocktail::graphics::ChromeTraceWriter* trace =
|
||||
hooks.active != nullptr ? hooks.active() : nullptr;
|
||||
const std::uint64_t pump_start_ns = trace != nullptr ? hooks.clock() : 0;
|
||||
g_native_call_messages_from_main_thread(
|
||||
env, g_native_gl_class_for_main_thread);
|
||||
if (trace != nullptr) {
|
||||
// Empty polls run at kilohertz rates and would swamp the trace, so only
|
||||
// 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) {
|
||||
hooks.slice(trace, "nativeCallMessagesFromMainThread", "pump",
|
||||
pump_start_ns, pump_end_ns, nullptr, 0);
|
||||
}
|
||||
}
|
||||
if (__builtin_expect(trace_pump, 0)) {
|
||||
if (pump_count <= 10 || pump_count % 100 == 0) {
|
||||
std::cerr << " [main] nativeCallMessagesFromMainThread returned #"
|
||||
@@ -1409,8 +1503,11 @@ void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) {
|
||||
|
||||
void PublishCurrentJniEnv(JNIEnv* env) {
|
||||
using SetCurrentJniEnvFn = void (*)(void*);
|
||||
auto* set_current_jni_env = reinterpret_cast<SetCurrentJniEnvFn>(
|
||||
::dlsym(RTLD_DEFAULT, "mocktail_set_current_jni_env"));
|
||||
// RTLD_DEFAULT resolves this to the runtime's own exported setter, so the
|
||||
// address is fixed for the life of the process.
|
||||
static SetCurrentJniEnvFn set_current_jni_env =
|
||||
reinterpret_cast<SetCurrentJniEnvFn>(
|
||||
::dlsym(RTLD_DEFAULT, "mocktail_set_current_jni_env"));
|
||||
if (set_current_jni_env) {
|
||||
set_current_jni_env(env);
|
||||
}
|
||||
@@ -2393,6 +2490,23 @@ void InstallNativeGlJavaImplementation(JNIEnv* env, jclass native_gl_java_class)
|
||||
|
||||
jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless);
|
||||
|
||||
mocktail::runtime::DisplaySize HostDisplaySize() {
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
}
|
||||
|
||||
// Live window pixels once the host window exists; the initial window size
|
||||
// handoff otherwise.
|
||||
mocktail::runtime::DisplaySize HostWindowPixelSize() {
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
if (viewport.valid()) {
|
||||
return {viewport.pixel_width, viewport.pixel_height};
|
||||
}
|
||||
return mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
|
||||
}
|
||||
|
||||
jobject BuildDeviceParams(JNIEnv* env) {
|
||||
jobject params = NewObject(env, "com/roblox/engine/jni/model/DeviceParams");
|
||||
if (!params) {
|
||||
@@ -2414,27 +2528,42 @@ jobject BuildDeviceParams(JNIEnv* env) {
|
||||
SetStringField(env, params, "appVersion", app_version.c_str());
|
||||
SetStringField(env, params, "country", "US");
|
||||
SetStringField(env, params, "manufacturer", manufacturer.c_str());
|
||||
SetStringField(env, params, "displayResolution", "1280x720");
|
||||
const mocktail::runtime::DisplaySize display_size =
|
||||
mocktail::runtime::ParseDisplaySize(
|
||||
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
|
||||
const std::string display_resolution = std::to_string(display_size.width) +
|
||||
"x" +
|
||||
std::to_string(display_size.height);
|
||||
SetStringField(env, params, "displayResolution", display_resolution.c_str());
|
||||
SetStringField(env, params, "networkType", "wifi");
|
||||
SetStringField(env, params, "deviceSku", device_sku.c_str());
|
||||
SetStringField(env, params, "socModel", soc_model.c_str());
|
||||
SetStringField(env, params, "appBuildVariant", "googleProdRelease");
|
||||
SetStringField(env, params, "testDeviceName", device_name.c_str());
|
||||
|
||||
const bool is_low_ram = !IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE");
|
||||
SetIntField(env, params, "deviceTotalMemoryMB", is_low_ram ? 2048 : 4096);
|
||||
SetIntField(env, params, "displayPhysicalWidthPixels", 1280);
|
||||
SetIntField(env, params, "displayPhysicalHeightPixels", 720);
|
||||
SetIntField(env, params, "memoryClass", is_low_ram ? 256 : 512);
|
||||
SetIntField(env, params, "largeMemoryClass", is_low_ram ? 512 : 1024);
|
||||
// MOCKTAIL_DISABLE_LOW_RAM_DEVICE forces the normal-RAM profile even when
|
||||
// the host total cannot be read or is below 4 GiB.
|
||||
std::uint64_t host_memory_bytes = mocktail::runtime::DetectHostMemoryBytes();
|
||||
if (IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE")) {
|
||||
host_memory_bytes =
|
||||
std::max<std::uint64_t>(host_memory_bytes, 4096ULL * 1024ULL * 1024ULL);
|
||||
}
|
||||
const mocktail::runtime::DeviceMemoryProfile memory =
|
||||
mocktail::runtime::BuildDeviceMemoryProfile(host_memory_bytes);
|
||||
SetIntField(env, params, "deviceTotalMemoryMB", memory.total_memory_mb);
|
||||
SetIntField(env, params, "displayPhysicalWidthPixels", display_size.width);
|
||||
SetIntField(env, params, "displayPhysicalHeightPixels",
|
||||
display_size.height);
|
||||
SetIntField(env, params, "memoryClass", memory.memory_class_mb);
|
||||
SetIntField(env, params, "largeMemoryClass", memory.large_memory_class_mb);
|
||||
SetLongField(env, params, "lowMemoryKillerBackgroundAppThreshold",
|
||||
is_low_ram ? 256 : 0);
|
||||
memory.low_memory_killer_background_threshold);
|
||||
SetLongField(env, params, "lowMemoryKillerForegroundAppThreshold",
|
||||
is_low_ram ? 512 : 0);
|
||||
memory.low_memory_killer_foreground_threshold);
|
||||
SetBooleanField(env, params, "cpu64Bit", JNI_TRUE);
|
||||
SetBooleanField(env, params, "isChrome", JNI_FALSE);
|
||||
SetBooleanField(env, params, "isLowRamDevice",
|
||||
is_low_ram ? JNI_TRUE : JNI_FALSE);
|
||||
memory.low_ram_device ? JNI_TRUE : JNI_FALSE);
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -2534,8 +2663,9 @@ jobject BuildAppBridgeInitParams(JNIEnv* env, jstring client_settings,
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetRobloxServiceUrlFields(env, params);
|
||||
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize init_display = HostDisplaySize();
|
||||
SetIntField(env, params, "screenWidth", init_display.width);
|
||||
SetIntField(env, params, "screenHeight", init_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "sdkVersion", 33);
|
||||
jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE;
|
||||
@@ -2569,13 +2699,19 @@ jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless) {
|
||||
SetStringField(env, params, "deviceName", platform_device_name.c_str());
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetStringField(env, params, "assetFolderPath", DefaultAssetPath().c_str());
|
||||
SetIntField(env, params, "width", 1280);
|
||||
SetIntField(env, params, "height", 720);
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize platform_window = HostWindowPixelSize();
|
||||
const mocktail::runtime::DisplaySize platform_display = HostDisplaySize();
|
||||
SetIntField(env, params, "width", platform_window.width);
|
||||
SetIntField(env, params, "height", platform_window.height);
|
||||
SetIntField(env, params, "screenWidth", platform_display.width);
|
||||
SetIntField(env, params, "screenHeight", platform_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "viewportWidthMm", 203);
|
||||
SetIntField(env, params, "viewportHeightMm", 114);
|
||||
SetIntField(env, params, "viewportWidthMm",
|
||||
mocktail::runtime::PixelsToMillimetersAt160Dpi(
|
||||
platform_window.width));
|
||||
SetIntField(env, params, "viewportHeightMm",
|
||||
mocktail::runtime::PixelsToMillimetersAt160Dpi(
|
||||
platform_window.height));
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
SetFloatField(env, params, "dpiScale", viewport.dpi_scale);
|
||||
@@ -2684,10 +2820,12 @@ jobject BuildStartAppParams(JNIEnv* env, jstring app_params,
|
||||
SetStringField(env, params, "deviceName", start_device_name.c_str());
|
||||
SetStringField(env, params, "locale", "en_us");
|
||||
SetRobloxServiceUrlFields(env, params);
|
||||
SetIntField(env, params, "width", 1280);
|
||||
SetIntField(env, params, "height", 720);
|
||||
SetIntField(env, params, "screenWidth", 1280);
|
||||
SetIntField(env, params, "screenHeight", 720);
|
||||
const mocktail::runtime::DisplaySize start_window = HostWindowPixelSize();
|
||||
const mocktail::runtime::DisplaySize start_display = HostDisplaySize();
|
||||
SetIntField(env, params, "width", start_window.width);
|
||||
SetIntField(env, params, "height", start_window.height);
|
||||
SetIntField(env, params, "screenWidth", start_display.width);
|
||||
SetIntField(env, params, "screenHeight", start_display.height);
|
||||
SetIntField(env, params, "densityDpi", 160);
|
||||
SetIntField(env, params, "sdkVersion", 33);
|
||||
SetIntField(env, params, "placeId", 0);
|
||||
@@ -2815,8 +2953,9 @@ jobject BuildMockSurface(JNIEnv* env) {
|
||||
if (!surface) {
|
||||
return nullptr;
|
||||
}
|
||||
SetIntField(env, surface, "width", 1280);
|
||||
SetIntField(env, surface, "height", 720);
|
||||
const mocktail::runtime::DisplaySize surface_size = HostWindowPixelSize();
|
||||
SetIntField(env, surface, "width", surface_size.width);
|
||||
SetIntField(env, surface, "height", surface_size.height);
|
||||
SetBooleanField(env, surface, "valid", JNI_TRUE);
|
||||
SetBooleanField(env, surface, "isValid", JNI_TRUE);
|
||||
return surface;
|
||||
@@ -3913,7 +4052,10 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n"
|
||||
on_surface_created(env, game_activity, handle, surface);
|
||||
}
|
||||
if (run_lifecycle_callbacks && on_surface_changed) {
|
||||
on_surface_changed(env, game_activity, handle, surface, 4, 1280, 720);
|
||||
const mocktail::runtime::DisplaySize changed_size =
|
||||
HostWindowPixelSize();
|
||||
on_surface_changed(env, game_activity, handle, surface, 4,
|
||||
changed_size.width, changed_size.height);
|
||||
}
|
||||
if (run_lifecycle_callbacks && on_surface_redraw_needed) {
|
||||
on_surface_redraw_needed(env, game_activity, handle, surface);
|
||||
@@ -4084,8 +4226,9 @@ std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n"
|
||||
ShouldRunStartupStep("MOCKTAIL_UPDATE_APP_UI_SIZES", false)) {
|
||||
env = ensure_env();
|
||||
std::cout << " [engine] updateAppUISizes\n" << std::flush;
|
||||
context->native_update_app_ui_sizes(env, native_gl_class, 1280, 720, 0, 0,
|
||||
0);
|
||||
const mocktail::runtime::DisplaySize ui_size = HostWindowPixelSize();
|
||||
context->native_update_app_ui_sizes(env, native_gl_class, ui_size.width,
|
||||
ui_size.height, 0, 0, 0);
|
||||
std::cout << " [engine] updateAppUISizes returned\n" << std::flush;
|
||||
}
|
||||
|
||||
@@ -4726,6 +4869,38 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
if (window_initialised) {
|
||||
auto* sdl_window =
|
||||
static_cast<SDL_Window*>(mocktail::window::GetBackendWindow());
|
||||
const SDL_DisplayID display =
|
||||
sdl_window != nullptr ? SDL_GetDisplayForWindow(sdl_window) : 0;
|
||||
const SDL_DisplayMode* mode =
|
||||
display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr;
|
||||
if (mode != nullptr && mode->w > 0 && mode->h > 0) {
|
||||
// SDL display modes are in desktop units; pixel_density maps them to
|
||||
// physical pixels.
|
||||
const float density = mode->pixel_density > 0.0f ? mode->pixel_density
|
||||
: 1.0f;
|
||||
const std::string display_size =
|
||||
std::to_string(std::lround(mode->w * density)) + "x" +
|
||||
std::to_string(std::lround(mode->h * density));
|
||||
setenv(mocktail::runtime::kDisplaySizeEnvironment, display_size.c_str(),
|
||||
1);
|
||||
std::cout << " [window] host display " << display_size << " pixels\n"
|
||||
<< std::flush;
|
||||
}
|
||||
}
|
||||
if (window_initialised) {
|
||||
const mocktail::window::WindowViewportSnapshot viewport =
|
||||
mocktail::window::GetWindowViewportSnapshot();
|
||||
if (viewport.valid()) {
|
||||
const std::string window_size = std::to_string(viewport.pixel_width) +
|
||||
"x" +
|
||||
std::to_string(viewport.pixel_height);
|
||||
setenv(mocktail::runtime::kWindowSizeEnvironment, window_size.c_str(),
|
||||
1);
|
||||
}
|
||||
}
|
||||
const SDL_SystemTheme system_theme =
|
||||
window_initialised ? SDL_GetSystemTheme() : SDL_SYSTEM_THEME_UNKNOWN;
|
||||
const bool system_dark_theme = system_theme == SDL_SYSTEM_THEME_DARK;
|
||||
@@ -6718,6 +6893,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
window_input_runtime =
|
||||
std::make_shared<mocktail::runtime::RobloxWindowInputRuntime>(
|
||||
input_environment, roblox_capabilities.input);
|
||||
window_input_runtime->SetRawMouseEnabled(input_capabilities.raw_mouse);
|
||||
const mocktail::Status input_status = window_input_runtime->Initialize();
|
||||
if (!input_status.ok()) {
|
||||
std::cerr << "[FATAL] Typed production input did not initialize: "
|
||||
@@ -6747,6 +6923,10 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
bool input_shutdown_completed = window_input_runtime == nullptr;
|
||||
bool resize_readiness_completed = true;
|
||||
if (mocktail::window::IsInitialised()) {
|
||||
std::cout << " [main] engine pump rest: "
|
||||
<< mocktail::runtime::EnginePumpRestModeName(
|
||||
mocktail::runtime::ActiveEnginePumpRestMode())
|
||||
<< " (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)\n";
|
||||
std::cout << " [main] entering SDL event loop (close the window to quit)\n"
|
||||
<< std::flush;
|
||||
std::unique_ptr<mocktail::window::WindowGameSurfaceBridge> surface_bridge;
|
||||
@@ -6862,7 +7042,14 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
const uint64_t fps_after_launch_ns = fps_trace ? MonotonicNanos() : 0;
|
||||
const uint64_t fps_pace_ns = mocktail::window::PaceInputPump();
|
||||
// The pacer sleeps only under throttled presentation. Unthrottled, the
|
||||
// engine pump above is a poll, and the rest between polls is what keeps
|
||||
// it from spinning.
|
||||
uint64_t fps_pace_ns = mocktail::window::PaceInputPump();
|
||||
if (fps_pace_ns == 0 &&
|
||||
mocktail::window::UnthrottledPresentationRequested()) {
|
||||
fps_pace_ns = mocktail::runtime::RestAfterEnginePump();
|
||||
}
|
||||
if (fps_trace) {
|
||||
const uint64_t fps_tick_end_ns = MonotonicNanos();
|
||||
++fps_samples;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits.h>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
@@ -20,8 +22,19 @@ namespace libc_shim {
|
||||
namespace {
|
||||
|
||||
std::unordered_map<std::string, std::string> g_path_mappings;
|
||||
// g_path_mappings ordered longest Android prefix first, so the first match
|
||||
// is the most specific one. Rebuilt whenever the mappings change.
|
||||
std::vector<std::pair<std::string, std::string>> g_ordered_mappings;
|
||||
std::vector<std::string> g_ca_bundle_aliases;
|
||||
|
||||
void RebuildOrderedMappings() {
|
||||
g_ordered_mappings.assign(g_path_mappings.begin(), g_path_mappings.end());
|
||||
std::stable_sort(g_ordered_mappings.begin(), g_ordered_mappings.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.first.size() > b.first.size();
|
||||
});
|
||||
}
|
||||
|
||||
bool g_installed = false;
|
||||
std::atomic<GuestAllocator> g_guest_allocator{nullptr};
|
||||
|
||||
@@ -72,11 +85,9 @@ std::string DefaultDataRoot() {
|
||||
return RuntimeRoot() + "/data";
|
||||
}
|
||||
|
||||
bool PrefixMatchesPath(const std::string& path, const std::string& prefix) {
|
||||
if (path.size() < prefix.size()) {
|
||||
return false;
|
||||
}
|
||||
if (path.compare(0, prefix.size(), prefix) != 0) {
|
||||
bool PrefixMatchesPath(std::string_view path, std::string_view prefix) {
|
||||
if (path.size() < prefix.size() ||
|
||||
std::memcmp(path.data(), prefix.data(), prefix.size()) != 0) {
|
||||
return false;
|
||||
}
|
||||
return path.size() == prefix.size() || path[prefix.size()] == '/';
|
||||
@@ -202,6 +213,7 @@ HostCaBundleResolution ResolveHostCaBundle() {
|
||||
|
||||
HostCaBundleResolution ConfigureHostCaBundlePathMappings() {
|
||||
for (const auto& alias : g_ca_bundle_aliases) g_path_mappings.erase(alias);
|
||||
RebuildOrderedMappings();
|
||||
g_ca_bundle_aliases.assign(kAndroidCaBundlePaths.begin(), kAndroidCaBundlePaths.end());
|
||||
if (const char* content = GetEnvNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
||||
auto root = std::filesystem::path(content).lexically_normal();
|
||||
@@ -297,31 +309,33 @@ void Install() {
|
||||
<< g_path_mappings.size() << " prefix mappings)\n";
|
||||
}
|
||||
|
||||
std::string TranslatePath(const std::string& android_path) {
|
||||
const std::string* best_android_prefix = nullptr;
|
||||
const std::string* best_host_prefix = nullptr;
|
||||
for (const auto& [android_prefix, host_prefix] : g_path_mappings) {
|
||||
if (PrefixMatchesPath(android_path, android_prefix) &&
|
||||
(best_android_prefix == nullptr ||
|
||||
android_prefix.size() > best_android_prefix->size())) {
|
||||
best_android_prefix = &android_prefix;
|
||||
best_host_prefix = &host_prefix;
|
||||
bool TranslatePathInto(std::string_view android_path, std::string* host_path) {
|
||||
for (const auto& [android_prefix, host_prefix] : g_ordered_mappings) {
|
||||
if (PrefixMatchesPath(android_path, android_prefix)) {
|
||||
const std::string_view rest = android_path.substr(android_prefix.size());
|
||||
host_path->reserve(host_prefix.size() + rest.size());
|
||||
host_path->assign(host_prefix);
|
||||
host_path->append(rest);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (best_android_prefix != nullptr && best_host_prefix != nullptr) {
|
||||
return *best_host_prefix +
|
||||
android_path.substr(best_android_prefix->size());
|
||||
}
|
||||
return android_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string TranslatePath(const std::string& android_path) {
|
||||
std::string host_path;
|
||||
return TranslatePathInto(android_path, &host_path) ? host_path : android_path;
|
||||
}
|
||||
|
||||
void RegisterPathMapping(const std::string& android_prefix,
|
||||
const std::string& host_prefix) {
|
||||
g_path_mappings[android_prefix] = host_prefix;
|
||||
RebuildOrderedMappings();
|
||||
}
|
||||
|
||||
void ClearPathMappings() {
|
||||
g_path_mappings.clear();
|
||||
g_ordered_mappings.clear();
|
||||
g_ca_bundle_aliases.clear();
|
||||
}
|
||||
|
||||
@@ -333,13 +347,16 @@ const char* HostPath(const char* path, std::string* storage) {
|
||||
if (path == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
*storage = libc_shim::TranslatePath(path);
|
||||
return storage->c_str();
|
||||
return libc_shim::TranslatePathInto(path, storage) ? storage->c_str() : path;
|
||||
}
|
||||
|
||||
// Read once: the guest calls these shims on every file operation.
|
||||
bool PathTraceEnabled() {
|
||||
const char* value = std::getenv("MOCKTAIL_PATH_TRACE");
|
||||
return value != nullptr && value[0] != '\0' && value[0] != '0';
|
||||
static const bool enabled = [] {
|
||||
const char* value = std::getenv("MOCKTAIL_PATH_TRACE");
|
||||
return value != nullptr && value[0] != '\0' && value[0] != '0';
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void TracePathCall(const char* name, const char* path, const char* host_path,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
@@ -551,6 +552,12 @@ int main(int argc, char* argv[]) {
|
||||
case mocktail::runtime::GameModeSessionState::kActive:
|
||||
std::cout << " [gamemode] performance request active\n";
|
||||
break;
|
||||
case mocktail::runtime::GameModeSessionState::kDeclinedCorePinning:
|
||||
std::cout << " [gamemode] released: it pinned this process to a subset "
|
||||
"of the CPUs, which concentrates the worker pool. Set "
|
||||
"pin_cores=no in ~/.config/gamemode.ini to keep GameMode, "
|
||||
"or performance.gamemode: on to accept the pinning\n";
|
||||
break;
|
||||
case mocktail::runtime::GameModeSessionState::kAlreadyActive:
|
||||
std::cout << " [gamemode] already active for this process\n";
|
||||
break;
|
||||
@@ -779,14 +786,20 @@ int main(int argc, char* argv[]) {
|
||||
<< " FFlag overrides from " << fflags_path << '\n';
|
||||
}
|
||||
std::string client_settings_overrides;
|
||||
std::vector<std::string> kept_fflag_overrides;
|
||||
if (!mocktail::runtime::MergeRuntimeClientSettingsOverrides(
|
||||
runtime_config.config.frame_rate(),
|
||||
runtime_config.config.performance(), fflags.json,
|
||||
&client_settings_overrides, &command_line_error)) {
|
||||
&client_settings_overrides, &command_line_error,
|
||||
&kept_fflag_overrides)) {
|
||||
std::cerr << "[FATAL] Cannot apply runtime client-settings policy: "
|
||||
<< command_line_error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
for (const std::string& name : kept_fflag_overrides) {
|
||||
std::cout << " [runtime] keeping " << name << " from " << fflags_path
|
||||
<< " over the performance preset\n";
|
||||
}
|
||||
std::string audio_capture_overrides;
|
||||
if (!mocktail::runtime::MergeAudioCaptureClientSettingsOverrides(
|
||||
runtime_config.config.microphone_enabled(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "runtime/command_line.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -78,6 +79,12 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) {
|
||||
result.options.allow_unverified_build = true;
|
||||
} else if (argument == "--force-run-latest") {
|
||||
result.options.force_run_latest = true;
|
||||
} else if (argument == "--profile") {
|
||||
if (!ReadOptionValue(argc, argv, &index, argument,
|
||||
&result.options.profile_trace_path,
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
} else if (argument == "--launch-uri") {
|
||||
if (!result.options.launch_request_json.empty()) {
|
||||
result.error = "duplicate option: --launch-uri";
|
||||
@@ -227,6 +234,20 @@ bool ApplyCommandLineEnvironment(const CommandLineOptions& options,
|
||||
error)) {
|
||||
return false;
|
||||
}
|
||||
if (!options.profile_trace_path.empty()) {
|
||||
// Absolute, so a later working-directory change cannot move the trace.
|
||||
std::error_code absolute_error;
|
||||
const std::filesystem::path trace_path =
|
||||
std::filesystem::absolute(options.profile_trace_path, absolute_error);
|
||||
if (absolute_error ||
|
||||
!SetEnvironment("MOCKTAIL_PROFILE_TRACE", trace_path.string(),
|
||||
error)) {
|
||||
if (error != nullptr && absolute_error) {
|
||||
*error = "could not resolve --profile path";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (options.window_mode == WindowMode::kHeadless) {
|
||||
return SetEnvironment("MOCKTAIL_HEADLESS", "1", error);
|
||||
}
|
||||
@@ -256,6 +277,8 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
"without approval; it is not activated\n"
|
||||
<< " --launch-uri <uri> Join from a roblox: or roblox-player: "
|
||||
"website link\n"
|
||||
<< " --profile <file> Write a Chrome trace of Vulkan adapter "
|
||||
"work for ui.perfetto.dev\n"
|
||||
<< " --help, -h Show this help\n\n"
|
||||
<< "Auth:\n"
|
||||
<< " When no saved Roblox session is found, Roblox's welcome screen "
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "runtime/device_memory_profile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t kMebibyte = 1024U * 1024U;
|
||||
constexpr std::uint64_t kMinimumNormalRamBytes = 4096U * kMebibyte;
|
||||
|
||||
} // namespace
|
||||
|
||||
DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes) {
|
||||
DeviceMemoryProfile profile;
|
||||
if (host_memory_bytes < kMinimumNormalRamBytes) {
|
||||
profile.total_memory_mb = 2048;
|
||||
profile.memory_class_mb = 256;
|
||||
profile.large_memory_class_mb = 512;
|
||||
profile.low_memory_killer_background_threshold = 256;
|
||||
profile.low_memory_killer_foreground_threshold = 512;
|
||||
profile.low_ram_device = true;
|
||||
return profile;
|
||||
}
|
||||
profile.total_memory_mb = static_cast<std::int32_t>(std::min<std::uint64_t>(
|
||||
host_memory_bytes / kMebibyte,
|
||||
static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max())));
|
||||
profile.memory_class_mb = 512;
|
||||
profile.large_memory_class_mb = 1024;
|
||||
return profile;
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -84,14 +84,50 @@ std::string DumpJson(const Json& value) {
|
||||
return value.dump(-1, ' ', false, Json::error_handler_t::replace);
|
||||
}
|
||||
|
||||
std::string RenderPlaceTemplate(std::string text, std::string_view place_name) {
|
||||
constexpr std::string_view placeholder = "{place_name}";
|
||||
// Expands in one pass, so placeholder text inside a place name stays literal.
|
||||
// A template that expands an empty placeholder renders empty, so "by
|
||||
// {creator_name}" never shows a dangling "by".
|
||||
std::string RenderPresenceTemplate(std::string_view text,
|
||||
std::string_view place_name,
|
||||
std::string_view place_icon,
|
||||
std::string_view creator_name) {
|
||||
constexpr std::string_view kPlaceName = "{place_name}";
|
||||
constexpr std::string_view kPlaceIcon = "{place_icon}";
|
||||
constexpr std::string_view kCreatorName = "{creator_name}";
|
||||
std::string rendered;
|
||||
rendered.reserve(text.size());
|
||||
std::size_t offset = 0;
|
||||
while ((offset = text.find(placeholder, offset)) != std::string::npos) {
|
||||
text.replace(offset, placeholder.size(), place_name);
|
||||
offset += place_name.size();
|
||||
const auto expand = [&](std::string_view token, std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
rendered += value;
|
||||
offset += token.size();
|
||||
return true;
|
||||
};
|
||||
while (offset < text.size()) {
|
||||
const std::string_view rest = text.substr(offset);
|
||||
if (rest.substr(0, kPlaceName.size()) == kPlaceName) {
|
||||
if (!expand(kPlaceName, place_name)) return {};
|
||||
} else if (rest.substr(0, kPlaceIcon.size()) == kPlaceIcon) {
|
||||
if (!expand(kPlaceIcon, place_icon)) return {};
|
||||
} else if (rest.substr(0, kCreatorName.size()) == kCreatorName) {
|
||||
if (!expand(kCreatorName, creator_name)) return {};
|
||||
} else {
|
||||
rendered.push_back(text[offset]);
|
||||
++offset;
|
||||
}
|
||||
}
|
||||
return TruncateUtf8(std::move(text), 128);
|
||||
return rendered;
|
||||
}
|
||||
|
||||
bool IsDiscordAssetKey(std::string_view key) {
|
||||
return !key.empty() && key.size() <= 32 &&
|
||||
std::all_of(key.begin(), key.end(), [](unsigned char byte) {
|
||||
return (byte >= 'a' && byte <= 'z') ||
|
||||
(byte >= 'A' && byte <= 'Z') ||
|
||||
(byte >= '0' && byte <= '9') || byte == '_' || byte == '-';
|
||||
});
|
||||
}
|
||||
|
||||
std::string PercentEncode(std::string_view value) {
|
||||
@@ -329,8 +365,14 @@ bool HandshakeDiscord(int descriptor, std::string_view application_id) {
|
||||
}
|
||||
|
||||
Json ActivityJson(const DiscordRpcActivity& activity) {
|
||||
Json result = {
|
||||
{"type", 0}, {"details", activity.details}, {"instance", true}};
|
||||
// Discord rejects the whole activity when a text field is an empty string.
|
||||
Json result = {{"type", 0}, {"instance", true}};
|
||||
if (!activity.details.empty()) {
|
||||
result["details"] = activity.details;
|
||||
}
|
||||
if (!activity.name.empty()) {
|
||||
result["name"] = activity.name;
|
||||
}
|
||||
if (!activity.state.empty()) {
|
||||
result["state"] = activity.state;
|
||||
}
|
||||
@@ -342,6 +384,12 @@ Json ActivityJson(const DiscordRpcActivity& activity) {
|
||||
if (!activity.large_text.empty()) {
|
||||
result["assets"]["large_text"] = activity.large_text;
|
||||
}
|
||||
if (!activity.small_image.empty()) {
|
||||
result["assets"]["small_image"] = activity.small_image;
|
||||
if (!activity.small_text.empty()) {
|
||||
result["assets"]["small_text"] = activity.small_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!activity.button_label.empty() && !activity.button_url.empty()) {
|
||||
result["buttons"] = Json::array(
|
||||
@@ -446,6 +494,7 @@ std::optional<Json> GetRobloxJson(std::string url) {
|
||||
|
||||
struct ResolvedPlaceMetadata {
|
||||
std::string name;
|
||||
std::string creator_name;
|
||||
std::string icon_url;
|
||||
};
|
||||
|
||||
@@ -494,6 +543,13 @@ ResolvedPlaceMetadata ResolvePlaceMetadata(int64_t place_id) {
|
||||
metadata.name =
|
||||
TruncateUtf8(first["name"].get<std::string>(), 128);
|
||||
}
|
||||
if (first.contains("creator") && first["creator"].is_object()) {
|
||||
const Json& creator = first["creator"];
|
||||
if (creator.contains("name") && creator["name"].is_string()) {
|
||||
metadata.creator_name =
|
||||
TruncateUtf8(creator["name"].get<std::string>(), 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::optional<Json> thumbnail = GetRobloxJson(
|
||||
@@ -542,46 +598,74 @@ std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request) {
|
||||
DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request, std::string place_name,
|
||||
int64_t session_started_at, std::string place_icon_url) {
|
||||
int64_t session_started_at, std::string place_icon_url,
|
||||
std::string creator_name) {
|
||||
const bool browsing = phase == RobloxExperiencePresencePhase::kBrowsing;
|
||||
const bool joining_without_place =
|
||||
phase == RobloxExperiencePresencePhase::kJoining && place_name.empty();
|
||||
if (!browsing && place_name.empty()) {
|
||||
place_name = config.text.unknown_place;
|
||||
}
|
||||
// Hidden or absent places expose nothing to any template, icons included.
|
||||
const bool show_place =
|
||||
!browsing && !joining_without_place && config.show_place_name;
|
||||
const std::string_view shown_name =
|
||||
show_place ? std::string_view(place_name) : std::string_view();
|
||||
const std::string_view shown_icon =
|
||||
show_place && IsSafeExternalImageUrl(place_icon_url)
|
||||
? std::string_view(place_icon_url)
|
||||
: std::string_view();
|
||||
const std::string_view shown_creator =
|
||||
show_place ? std::string_view(creator_name) : std::string_view();
|
||||
const auto render = [&](std::string_view text) {
|
||||
return RenderPresenceTemplate(text, shown_name, shown_icon,
|
||||
shown_creator);
|
||||
};
|
||||
const auto render_text = [&](std::string_view text) {
|
||||
return TruncateUtf8(render(text), 128);
|
||||
};
|
||||
const auto is_image = [](std::string_view image) {
|
||||
return IsSafeExternalImageUrl(image) || IsDiscordAssetKey(image);
|
||||
};
|
||||
|
||||
DiscordRpcActivity activity;
|
||||
activity.state = TruncateUtf8(config.text.state, 128);
|
||||
switch (phase) {
|
||||
case RobloxExperiencePresencePhase::kBrowsing:
|
||||
activity.details = TruncateUtf8(config.text.browsing, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kJoining:
|
||||
case RobloxExperiencePresencePhase::kPlaying: {
|
||||
if (place_name.empty()) {
|
||||
if (phase == RobloxExperiencePresencePhase::kJoining) {
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
}
|
||||
place_name = config.text.unknown_place;
|
||||
}
|
||||
if (config.show_place_name) {
|
||||
activity.details = RenderPlaceTemplate(config.text.playing, place_name);
|
||||
} else {
|
||||
activity.details = activity.state;
|
||||
activity.state.clear();
|
||||
}
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.show_elapsed_time && session_started_at > 0) {
|
||||
activity.start_timestamp = session_started_at;
|
||||
}
|
||||
if (IsSafeExternalImageUrl(place_icon_url)) {
|
||||
activity.large_image = std::move(place_icon_url);
|
||||
activity.large_text = TruncateUtf8(place_name, 128);
|
||||
}
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.join_enabled && request != nullptr &&
|
||||
(!config.public_servers_only || IsPublicDiscordJoin(*request))) {
|
||||
activity.button_url = BuildDiscordJoinUrl(*request);
|
||||
if (!activity.button_url.empty()) {
|
||||
activity.button_label = config.join_button_label;
|
||||
}
|
||||
}
|
||||
break;
|
||||
activity.name = render_text(config.text.title);
|
||||
activity.state = render_text(config.text.state);
|
||||
if (browsing) {
|
||||
activity.details = render_text(config.text.browsing);
|
||||
} else if (joining_without_place) {
|
||||
activity.details = render_text(config.text.joining);
|
||||
} else {
|
||||
if (config.show_place_name) {
|
||||
activity.details = render_text(config.text.playing);
|
||||
} else {
|
||||
activity.details = std::move(activity.state);
|
||||
activity.state.clear();
|
||||
}
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.show_elapsed_time && session_started_at > 0) {
|
||||
activity.start_timestamp = session_started_at;
|
||||
}
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.join_enabled && request != nullptr &&
|
||||
(!config.public_servers_only || IsPublicDiscordJoin(*request))) {
|
||||
activity.button_url = BuildDiscordJoinUrl(*request);
|
||||
if (!activity.button_url.empty()) {
|
||||
activity.button_label = config.join_button_label;
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.large_image = render(config.images.large);
|
||||
if (!is_image(activity.large_image)) {
|
||||
activity.large_image.clear();
|
||||
return activity;
|
||||
}
|
||||
activity.large_text = render_text(config.images.large_text);
|
||||
activity.small_image = render(config.images.small);
|
||||
if (is_image(activity.small_image)) {
|
||||
activity.small_text = render_text(config.images.small_text);
|
||||
} else {
|
||||
activity.small_image.clear();
|
||||
}
|
||||
return activity;
|
||||
}
|
||||
@@ -697,6 +781,7 @@ class DiscordRpcSession::Impl final {
|
||||
|
||||
struct CachedPlaceMetadata {
|
||||
std::string name;
|
||||
std::string creator_name;
|
||||
std::string icon_url;
|
||||
std::chrono::steady_clock::time_point next_attempt{};
|
||||
unsigned failures = 0;
|
||||
@@ -734,6 +819,7 @@ class DiscordRpcSession::Impl final {
|
||||
Desired desired = Snapshot();
|
||||
std::string place_name;
|
||||
std::string place_icon_url;
|
||||
std::string creator_name;
|
||||
bool resolve_metadata_after_publish = false;
|
||||
if ((desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying) &&
|
||||
@@ -742,6 +828,7 @@ class DiscordRpcSession::Impl final {
|
||||
place_metadata_[desired.request.place_id];
|
||||
place_name = cached.name;
|
||||
place_icon_url = cached.icon_url;
|
||||
creator_name = cached.creator_name;
|
||||
if ((cached.name.empty() || cached.icon_url.empty()) &&
|
||||
std::chrono::steady_clock::now() >= cached.next_attempt) {
|
||||
resolve_metadata_after_publish = true;
|
||||
@@ -787,7 +874,7 @@ class DiscordRpcSession::Impl final {
|
||||
? nullptr
|
||||
: &desired.request,
|
||||
std::move(place_name), desired.started_at,
|
||||
std::move(place_icon_url));
|
||||
std::move(place_icon_url), std::move(creator_name));
|
||||
if (!SetDiscordActivity(descriptor, &activity, nonce++)) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
@@ -811,6 +898,11 @@ class DiscordRpcSession::Impl final {
|
||||
cached.name = resolved.name;
|
||||
changed = true;
|
||||
}
|
||||
if (!resolved.creator_name.empty() &&
|
||||
resolved.creator_name != cached.creator_name) {
|
||||
cached.creator_name = resolved.creator_name;
|
||||
changed = true;
|
||||
}
|
||||
if (!resolved.icon_url.empty() &&
|
||||
resolved.icon_url != cached.icon_url) {
|
||||
cached.icon_url = resolved.icon_url;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#include "runtime/engine_pump_rest.h"
|
||||
|
||||
#include <cpuid.h>
|
||||
#include <immintrin.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <time.h>
|
||||
#include <x86intrin.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
namespace {
|
||||
|
||||
uint64_t NowNs() {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return static_cast<uint64_t>(now.tv_sec) * 1'000'000'000ULL +
|
||||
static_cast<uint64_t>(now.tv_nsec);
|
||||
}
|
||||
|
||||
// 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.
|
||||
__attribute__((target("waitpkg"))) void TpauseUntil(uint64_t deadline_ns) {
|
||||
while (NowNs() < deadline_ns) {
|
||||
_tpause(0, __rdtsc() + 100000ULL);
|
||||
}
|
||||
}
|
||||
|
||||
void LowerTimerSlackOnce() {
|
||||
// Only the main thread rests, so the slack is set once per process.
|
||||
static const bool slack_set = [] {
|
||||
return prctl(PR_SET_TIMERSLACK, kEnginePumpTimerSlackNs) == 0;
|
||||
}();
|
||||
(void)slack_set;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* EnginePumpRestModeName(EnginePumpRestMode mode) {
|
||||
switch (mode) {
|
||||
case EnginePumpRestMode::kSleep:
|
||||
return "sleep";
|
||||
case EnginePumpRestMode::kTpause:
|
||||
return "tpause";
|
||||
case EnginePumpRestMode::kSpin:
|
||||
return "spin";
|
||||
}
|
||||
return "spin";
|
||||
}
|
||||
|
||||
EnginePumpRestMode ChooseEnginePumpRestMode(
|
||||
const EnginePumpRestInputs& inputs) {
|
||||
if (inputs.override != nullptr) {
|
||||
if (std::strcmp(inputs.override, "sleep") == 0) {
|
||||
return EnginePumpRestMode::kSleep;
|
||||
}
|
||||
if (std::strcmp(inputs.override, "spin") == 0) {
|
||||
return EnginePumpRestMode::kSpin;
|
||||
}
|
||||
if (std::strcmp(inputs.override, "tpause") == 0 &&
|
||||
inputs.cpu_has_waitpkg) {
|
||||
return EnginePumpRestMode::kTpause;
|
||||
}
|
||||
}
|
||||
if (inputs.governor_performance) {
|
||||
return EnginePumpRestMode::kSleep;
|
||||
}
|
||||
return inputs.cpu_has_waitpkg ? EnginePumpRestMode::kTpause
|
||||
: EnginePumpRestMode::kSpin;
|
||||
}
|
||||
|
||||
bool CpuGovernorIsPerformance(const char* path) {
|
||||
std::FILE* file = std::fopen(path, "r");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
char governor[32] = {};
|
||||
const bool read = std::fgets(governor, sizeof(governor), file) != nullptr;
|
||||
std::fclose(file);
|
||||
if (!read) {
|
||||
return false;
|
||||
}
|
||||
governor[std::strcspn(governor, "\r\n")] = '\0';
|
||||
return std::strcmp(governor, "performance") == 0;
|
||||
}
|
||||
|
||||
bool CpuHasWaitPkg() {
|
||||
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;
|
||||
}
|
||||
|
||||
EnginePumpRestMode ActiveEnginePumpRestMode() {
|
||||
static const EnginePumpRestMode mode = [] {
|
||||
EnginePumpRestInputs inputs;
|
||||
inputs.governor_performance = CpuGovernorIsPerformance(
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
|
||||
inputs.cpu_has_waitpkg = CpuHasWaitPkg();
|
||||
inputs.override = std::getenv("MOCKTAIL_ENGINE_PUMP_REST");
|
||||
return ChooseEnginePumpRestMode(inputs);
|
||||
}();
|
||||
return mode;
|
||||
}
|
||||
|
||||
uint64_t RestAfterEnginePump(EnginePumpRestMode mode) {
|
||||
LowerTimerSlackOnce();
|
||||
switch (mode) {
|
||||
case EnginePumpRestMode::kSpin:
|
||||
return 0;
|
||||
case EnginePumpRestMode::kTpause:
|
||||
TpauseUntil(NowNs() + kEnginePumpRestNs);
|
||||
return kEnginePumpRestNs;
|
||||
case EnginePumpRestMode::kSleep:
|
||||
break;
|
||||
}
|
||||
timespec rest{};
|
||||
rest.tv_sec = 0;
|
||||
rest.tv_nsec = static_cast<long>(kEnginePumpRestNs);
|
||||
// A signal may cut the rest short. The remainder is not slept again: an
|
||||
// early poll only costs a few microseconds.
|
||||
(void)clock_nanosleep(CLOCK_MONOTONIC, 0, &rest, nullptr);
|
||||
return kEnginePumpRestNs;
|
||||
}
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
@@ -349,6 +349,7 @@ class ExternalLaunchBroker::Impl final {
|
||||
Status initial_status = NormalizeRequest(*initial_request, &normalized);
|
||||
if (!initial_status.ok()) return initial_status;
|
||||
pending_.push_back(std::move(normalized));
|
||||
PublishPendingCount();
|
||||
}
|
||||
socket_path_ = options_.socket_path;
|
||||
Status status = socket_path_.empty()
|
||||
@@ -426,6 +427,14 @@ class ExternalLaunchBroker::Impl final {
|
||||
if (!sink.valid()) {
|
||||
return Invalid("external-launch sink is incomplete");
|
||||
}
|
||||
// The host main loop drains on every tick and the queue is almost always
|
||||
// empty. Observe that without either lock, so an idle tick never waits
|
||||
// behind a dispatch already in flight. A request queued just after this
|
||||
// load is delivered on the next tick.
|
||||
if (pending_count_.load(std::memory_order_acquire) == 0 &&
|
||||
!stopping_.load(std::memory_order_acquire)) {
|
||||
return Status::Ok();
|
||||
}
|
||||
std::lock_guard<std::mutex> drain_lock(drain_mutex_);
|
||||
if (stopping_.load(std::memory_order_acquire)) {
|
||||
return Failed("external-launch broker is stopping");
|
||||
@@ -437,11 +446,13 @@ class ExternalLaunchBroker::Impl final {
|
||||
if (pending_.empty()) break;
|
||||
request = std::move(pending_.front());
|
||||
pending_.pop_front();
|
||||
PublishPendingCount();
|
||||
}
|
||||
Status status = sink.dispatch(sink.context, request);
|
||||
if (!status.ok()) {
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
pending_.push_front(std::move(request));
|
||||
PublishPendingCount();
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -482,6 +493,7 @@ class ExternalLaunchBroker::Impl final {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
pending_.clear();
|
||||
PublishPendingCount();
|
||||
}
|
||||
started_ = false;
|
||||
return Status::Ok();
|
||||
@@ -577,9 +589,16 @@ class ExternalLaunchBroker::Impl final {
|
||||
return Unavailable("external-launch queue is full");
|
||||
}
|
||||
pending_.push_back(std::move(request));
|
||||
PublishPendingCount();
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
// Mirrors pending_.size() for Drain's lock-free idle check. Every caller
|
||||
// holds queue_mutex_, except Start, which runs before the worker exists.
|
||||
void PublishPendingCount() {
|
||||
pending_count_.store(pending_.size(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void CleanupEndpoint() {
|
||||
CloseDescriptor(&stop_event_);
|
||||
CloseDescriptor(&listener_);
|
||||
@@ -607,6 +626,7 @@ class ExternalLaunchBroker::Impl final {
|
||||
std::mutex drain_mutex_;
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::deque<RobloxExperienceLaunchRequest> pending_;
|
||||
std::atomic<std::size_t> pending_count_{0};
|
||||
OwnedPthread worker_;
|
||||
std::atomic<bool> stopping_{true};
|
||||
int listener_ = -1;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "runtime/game_mode.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#if defined(__GLIBC__)
|
||||
#include <link.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -56,8 +59,63 @@ void* OpenClientLibrary(const char* name) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int GetThreadAffinity(cpu_set_t* mask) {
|
||||
return sched_getaffinity(0, sizeof(*mask), mask);
|
||||
}
|
||||
|
||||
// sched_setaffinity binds one thread, and GameMode pins every thread it finds,
|
||||
// so restoring the mask means walking the task list. Threads that exit while
|
||||
// the directory is being read are skipped, not treated as failures.
|
||||
int SetAffinityOfEveryThread(const cpu_set_t* mask) {
|
||||
DIR* tasks = opendir("/proc/self/task");
|
||||
if (tasks == nullptr) {
|
||||
return sched_setaffinity(0, sizeof(*mask), mask);
|
||||
}
|
||||
int result = 0;
|
||||
for (;;) {
|
||||
// readdir() reports both end-of-directory and failure with a null return,
|
||||
// so a read that dies partway must not pass for a completed walk.
|
||||
errno = 0;
|
||||
const dirent* entry = readdir(tasks);
|
||||
if (entry == nullptr) {
|
||||
if (errno != 0) {
|
||||
result = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const long tid = std::strtol(entry->d_name, nullptr, 10);
|
||||
if (tid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (sched_setaffinity(static_cast<pid_t>(tid), sizeof(*mask), mask) != 0 &&
|
||||
errno != ESRCH) {
|
||||
result = -1;
|
||||
}
|
||||
}
|
||||
closedir(tasks);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CpuAffinityApi HostCpuAffinityApi() {
|
||||
return CpuAffinityApi{&GetThreadAffinity, &SetAffinityOfEveryThread};
|
||||
}
|
||||
|
||||
bool CpuAffinityNarrowed(const cpu_set_t& before, const cpu_set_t& after) {
|
||||
if (CPU_COUNT(&after) >= CPU_COUNT(&before)) {
|
||||
return false;
|
||||
}
|
||||
// A smaller set that escapes `before` is a move, not a narrowing, and the
|
||||
// daemon's choice of CPUs is then the only one known to be permitted.
|
||||
for (int cpu = 0; cpu < CPU_SETSIZE; ++cpu) {
|
||||
if (CPU_ISSET(cpu, &after) && !CPU_ISSET(cpu, &before)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseGameModePolicy(std::string_view value, GameModePolicy* policy) {
|
||||
if (policy == nullptr) {
|
||||
return false;
|
||||
@@ -97,6 +155,8 @@ const char* GameModeSessionStateName(GameModeSessionState state) {
|
||||
return "unavailable";
|
||||
case GameModeSessionState::kRequestFailed:
|
||||
return "request-failed";
|
||||
case GameModeSessionState::kDeclinedCorePinning:
|
||||
return "declined-core-pinning";
|
||||
case GameModeSessionState::kAlreadyActive:
|
||||
return "already-active";
|
||||
case GameModeSessionState::kActive:
|
||||
@@ -123,7 +183,7 @@ GameModeSession::GameModeSession(GameModeSession&& other) noexcept
|
||||
|
||||
GameModeSession GameModeSession::Start(GameModePolicy policy) {
|
||||
if (policy == GameModePolicy::kOff) {
|
||||
return StartBound(policy, {}, nullptr);
|
||||
return StartBound(policy, {}, {}, nullptr);
|
||||
}
|
||||
|
||||
void* library = OpenClientLibrary("libgamemode.so.0");
|
||||
@@ -155,7 +215,8 @@ GameModeSession GameModeSession::Start(GameModePolicy policy) {
|
||||
(void)ResolveFunction(library, "real_gamemode_query_status",
|
||||
&api.query_status, nullptr);
|
||||
|
||||
GameModeSession session = StartBound(policy, api, library);
|
||||
GameModeSession session =
|
||||
StartBound(policy, api, HostCpuAffinityApi(), library);
|
||||
if (!session.owns_request_) {
|
||||
session.CloseLibrary();
|
||||
}
|
||||
@@ -164,11 +225,17 @@ GameModeSession GameModeSession::Start(GameModePolicy policy) {
|
||||
|
||||
GameModeSession GameModeSession::StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api) {
|
||||
return StartBound(policy, api, nullptr);
|
||||
return StartBound(policy, api, {}, nullptr);
|
||||
}
|
||||
|
||||
GameModeSession GameModeSession::StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api, CpuAffinityApi affinity) {
|
||||
return StartBound(policy, api, affinity, nullptr);
|
||||
}
|
||||
|
||||
GameModeSession GameModeSession::StartBound(GameModePolicy policy,
|
||||
GameModeClientApi api,
|
||||
CpuAffinityApi affinity,
|
||||
void* library_handle) {
|
||||
GameModeSession session;
|
||||
if (policy == GameModePolicy::kOff) {
|
||||
@@ -185,6 +252,11 @@ GameModeSession GameModeSession::StartBound(GameModePolicy policy,
|
||||
session.state_ = GameModeSessionState::kAlreadyActive;
|
||||
return session;
|
||||
}
|
||||
cpu_set_t before;
|
||||
CPU_ZERO(&before);
|
||||
const bool affinity_readable =
|
||||
policy != GameModePolicy::kOn && affinity.get != nullptr &&
|
||||
affinity.set != nullptr && affinity.get(&before) == 0;
|
||||
if (api.request_start() != 0) {
|
||||
session.state_ = GameModeSessionState::kRequestFailed;
|
||||
session.detail_ = ClientError(api, "GameMode request was rejected");
|
||||
@@ -192,6 +264,21 @@ GameModeSession GameModeSession::StartBound(GameModePolicy policy,
|
||||
}
|
||||
session.state_ = GameModeSessionState::kActive;
|
||||
session.owns_request_ = true;
|
||||
// The daemon pins before the request call returns, and re-pins every few
|
||||
// seconds afterwards. Ending the request is what releases the pin.
|
||||
cpu_set_t after;
|
||||
CPU_ZERO(&after);
|
||||
if (affinity_readable && affinity.get(&after) == 0 &&
|
||||
CpuAffinityNarrowed(before, after)) {
|
||||
session.state_ = GameModeSessionState::kDeclinedCorePinning;
|
||||
session.detail_ = "GameMode pinned the process to a subset of its CPUs";
|
||||
// A release that fails leaves the daemon holding the request, so keep
|
||||
// owning it and let Stop() retry rather than reporting it released.
|
||||
if (api.request_end() == 0) {
|
||||
session.owns_request_ = false;
|
||||
}
|
||||
(void)affinity.set(&before);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,10 @@ namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
// TextureManager2 selects its mobile MipPackStream path on Android Vulkan and
|
||||
// can keep world textures at low-resolution mips. TM1 preserves full texture
|
||||
// fidelity until that path is compatible with the host Vulkan adapter.
|
||||
// Keep the Vulkan shader workaround narrowly scoped. TextureManager2 is left
|
||||
// enabled so the runtime can use its normal texture path and mip selection.
|
||||
constexpr char kVulkanClientSettingsOverrides[] =
|
||||
R"({"FStringGraphicsTextureManager2DenyPattern2":".*","FStringGraphicsVulkanShaderMTDenyPattern":"4318:.*"})";
|
||||
R"({"FStringGraphicsVulkanShaderMTDenyPattern":"4318:.*"})";
|
||||
|
||||
constexpr const char* kIcdDirectories[] = {
|
||||
"/usr/share/vulkan/icd.d",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include "runtime/performance_policy.h"
|
||||
|
||||
#include <sched.h>
|
||||
@@ -27,22 +28,21 @@ struct ClientSetting {
|
||||
std::string_view value;
|
||||
};
|
||||
|
||||
bool SetCompatibleValue(nlohmann::json* object, const ClientSetting& setting,
|
||||
std::string* error) {
|
||||
// A caller-supplied value always wins over the preset; the key is reported
|
||||
// through kept_overrides so startup can say which preset values were skipped.
|
||||
void SetCompatibleValue(nlohmann::json* object, const ClientSetting& setting,
|
||||
std::vector<std::string>* kept_overrides) {
|
||||
const std::string name(setting.name);
|
||||
const std::string value(setting.value);
|
||||
const auto existing = object->find(name);
|
||||
if (existing != object->end()) {
|
||||
if (!existing->is_string() || existing->get<std::string>() != value) {
|
||||
if (error != nullptr) {
|
||||
*error = std::string("performance policy conflicts with ") + name;
|
||||
}
|
||||
return false;
|
||||
if ((!existing->is_string() || existing->get<std::string>() != value) &&
|
||||
kept_overrides != nullptr) {
|
||||
kept_overrides->push_back(name);
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
(*object)[name] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadTopologyValue(int logical_cpu, std::string_view name, int* value) {
|
||||
@@ -154,10 +154,10 @@ int DetectAvailablePhysicalCoreCount() {
|
||||
: static_cast<int>(physical_cores.size());
|
||||
}
|
||||
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error) {
|
||||
bool MergePerformanceClientSettingsOverrides(
|
||||
const PerformancePolicy& policy, std::string_view base_json,
|
||||
std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides) {
|
||||
if (merged_json == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "performance client-settings output is required";
|
||||
@@ -184,13 +184,10 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
return true;
|
||||
}
|
||||
const auto apply_settings = [&overrides,
|
||||
error](const auto& settings) -> bool {
|
||||
kept_overrides](const auto& settings) {
|
||||
for (const ClientSetting& setting : settings) {
|
||||
if (!SetCompatibleValue(&overrides, setting, error)) {
|
||||
return false;
|
||||
}
|
||||
SetCompatibleValue(&overrides, setting, kept_overrides);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const bool throughput_mode =
|
||||
policy.physics_worker_mode == PhysicsWorkerMode::kThroughput;
|
||||
@@ -198,9 +195,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
const std::array<ClientSetting, 1> physics_settings = {{
|
||||
{"DFIntSimMidPhaseContactPipelineBatchSize", "128"},
|
||||
}};
|
||||
if (!apply_settings(physics_settings)) {
|
||||
return false;
|
||||
}
|
||||
apply_settings(physics_settings);
|
||||
}
|
||||
int render_worker_count = 0;
|
||||
if (throughput_mode ||
|
||||
@@ -223,9 +218,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
{"FIntTaskSchedulerAutoThreadLimit", workers},
|
||||
{"DFIntSimMidPhaseContactPipelineBatchSize", "128"},
|
||||
}};
|
||||
if (!apply_settings(physics_settings)) {
|
||||
return false;
|
||||
}
|
||||
apply_settings(physics_settings);
|
||||
} else if (policy.multithreaded_rendering &&
|
||||
policy.physics_worker_mode != PhysicsWorkerMode::kLatency) {
|
||||
const std::string workers = std::to_string(render_worker_count);
|
||||
@@ -233,9 +226,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
{"FIntTaskSchedulerThreadMin", workers},
|
||||
{"FIntTaskSchedulerAsyncTasksMinimumThreadCount", workers},
|
||||
}};
|
||||
if (!apply_settings(scheduler_settings)) {
|
||||
return false;
|
||||
}
|
||||
apply_settings(scheduler_settings);
|
||||
}
|
||||
if ((policy.multithreaded_rendering || throughput_mode) &&
|
||||
policy.physics_worker_mode != PhysicsWorkerMode::kLatency) {
|
||||
@@ -322,26 +313,21 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
{"FIntDefaultAudioDecodeBufferSizeMs", "50"},
|
||||
{"FIntMaxAudibleSoundChannels", "32"},
|
||||
}};
|
||||
if (!apply_settings(rendering_settings)) {
|
||||
return false;
|
||||
}
|
||||
apply_settings(rendering_settings);
|
||||
if (!manual_quality) {
|
||||
if (!SetCompatibleValue(
|
||||
&overrides, {"FIntDebugFRMQualityLevelOverride", quality_str},
|
||||
error)) {
|
||||
return false;
|
||||
}
|
||||
SetCompatibleValue(&overrides,
|
||||
{"FIntDebugFRMQualityLevelOverride", quality_str},
|
||||
kept_overrides);
|
||||
}
|
||||
}
|
||||
*merged_json = overrides.dump();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error) {
|
||||
bool MergeRuntimeClientSettingsOverrides(
|
||||
const FrameRatePolicy& frame_rate, const PerformancePolicy& performance,
|
||||
std::string_view base_json, std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides) {
|
||||
std::string frame_rate_overrides;
|
||||
if (!MergeFrameRateClientSettingsOverrides(frame_rate, base_json,
|
||||
&frame_rate_overrides, error)) {
|
||||
@@ -349,7 +335,8 @@ bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
}
|
||||
std::string performance_overrides;
|
||||
if (!MergePerformanceClientSettingsOverrides(
|
||||
performance, frame_rate_overrides, &performance_overrides, error)) {
|
||||
performance, frame_rate_overrides, &performance_overrides, error,
|
||||
kept_overrides)) {
|
||||
return false;
|
||||
}
|
||||
std::string http_client_overrides;
|
||||
|
||||
@@ -627,6 +627,10 @@ RobloxInputDispatchResult RobloxInputRuntime::HandleEvent(
|
||||
return result;
|
||||
}
|
||||
|
||||
void RobloxInputRuntime::SetRawMouseEnabled(bool enabled) {
|
||||
router_.SetRawMouseEnabled(enabled);
|
||||
}
|
||||
|
||||
RobloxInputSnapshot RobloxInputRuntime::Snapshot() const {
|
||||
return router_.Snapshot();
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ Status RobloxInputRouter::Activate(RobloxInputViewport viewport,
|
||||
gamepads_.clear();
|
||||
mouse_x_ = 0.0f;
|
||||
mouse_y_ = 0.0f;
|
||||
pointer_anchored_ = false;
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
@@ -532,6 +533,11 @@ RobloxInputSnapshot RobloxInputRouter::Snapshot() const {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void RobloxInputRouter::SetRawMouseEnabled(bool enabled) {
|
||||
const std::lock_guard<std::mutex> lock(mutex_);
|
||||
raw_mouse_ = enabled;
|
||||
}
|
||||
|
||||
RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
|
||||
const platform::MouseMotionEvent& event) {
|
||||
if (sink_.mouse_move == nullptr) {
|
||||
@@ -544,21 +550,31 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
|
||||
const float delta_x = transform.HostLogicalToGuestX(event.delta_x);
|
||||
const float delta_y = transform.HostLogicalToGuestY(event.delta_y);
|
||||
|
||||
// Relative-mode motion reports zero absolute coordinates and a delta. The
|
||||
// pointer holds its captured position while the delta passes through, so a
|
||||
// long camera turn neither parks the reported position at the viewport edge
|
||||
// nor swallows the reversal. Roblox draws its cursor at the held position
|
||||
// when the capture ends, which matches where the host pointer is warped.
|
||||
if (event.x == 0.0f && event.y == 0.0f &&
|
||||
(delta_x != 0.0f || delta_y != 0.0f)) {
|
||||
mouse_x_ += delta_x;
|
||||
mouse_y_ += delta_y;
|
||||
HoldPointerLocked(transform);
|
||||
} else {
|
||||
mouse_x_ = transform.HostLogicalToGuestX(event.x);
|
||||
mouse_y_ = transform.HostLogicalToGuestY(event.y);
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.x), 0.0f, max_x);
|
||||
mouse_y_ = std::clamp(transform.HostLogicalToGuestY(event.y), 0.0f, max_y);
|
||||
pointer_anchored_ = true;
|
||||
}
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
const float clamped_x = std::clamp(mouse_x_, 0.0f, max_x);
|
||||
const float clamped_y = std::clamp(mouse_y_, 0.0f, max_y);
|
||||
|
||||
// Raw mouse reports the host's own delta, skipping the guest-surface scale
|
||||
// that otherwise changes look sensitivity when the desktop display scale and
|
||||
// the window's pixel density disagree. The position stays in guest units.
|
||||
const float reported_delta_x = raw_mouse_ ? event.delta_x : delta_x;
|
||||
const float reported_delta_y = raw_mouse_ ? event.delta_y : delta_y;
|
||||
|
||||
return NativeResultLocked(
|
||||
sink_.mouse_move(sink_.context, clamped_x, clamped_y, delta_x, delta_y),
|
||||
sink_.mouse_move(sink_.context, mouse_x_, mouse_y_, reported_delta_x,
|
||||
reported_delta_y),
|
||||
RobloxInputEventKind::kMouseMotion);
|
||||
}
|
||||
|
||||
@@ -575,19 +591,21 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked(
|
||||
RobloxInputEventKind::kMouseButton,
|
||||
Unsupported("SDL mouse button has no Android mapping"));
|
||||
}
|
||||
if (event.x > 0.0f || event.y > 0.0f ||
|
||||
(mouse_x_ == 0.0f && mouse_y_ == 0.0f)) {
|
||||
const platform::SurfaceCoordinateTransform transform =
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
mouse_x_ = transform.HostLogicalToGuestX(event.x);
|
||||
mouse_y_ = transform.HostLogicalToGuestY(event.y);
|
||||
}
|
||||
const platform::SurfaceCoordinateTransform transform =
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
const float clamped_x = std::clamp(mouse_x_, 0.0f, max_x);
|
||||
const float clamped_y = std::clamp(mouse_y_, 0.0f, max_y);
|
||||
// Button events during pointer capture carry zero coordinates and must not
|
||||
// move the held position.
|
||||
if (event.x > 0.0f || event.y > 0.0f) {
|
||||
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.x), 0.0f, max_x);
|
||||
mouse_y_ = std::clamp(transform.HostLogicalToGuestY(event.y), 0.0f, max_y);
|
||||
pointer_anchored_ = true;
|
||||
} else {
|
||||
HoldPointerLocked(transform);
|
||||
}
|
||||
const float clamped_x = mouse_x_;
|
||||
const float clamped_y = mouse_y_;
|
||||
if (event.pressed) {
|
||||
const RobloxTextEditorSnapshot text = text_editor_.Snapshot();
|
||||
if (text.focused) {
|
||||
@@ -620,13 +638,38 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseWheelLocked(
|
||||
}
|
||||
const platform::SurfaceCoordinateTransform transform =
|
||||
CoordinateTransform(snapshot_.viewport);
|
||||
mouse_x_ = transform.HostLogicalToGuestX(std::max(0.0f, event.mouse_x));
|
||||
mouse_y_ = transform.HostLogicalToGuestY(std::max(0.0f, event.mouse_y));
|
||||
// Wheel events during pointer capture carry zero coordinates and must not
|
||||
// move the held position.
|
||||
if (event.mouse_x > 0.0f || event.mouse_y > 0.0f) {
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.mouse_x), 0.0f,
|
||||
max_x);
|
||||
mouse_y_ = std::clamp(transform.HostLogicalToGuestY(event.mouse_y), 0.0f,
|
||||
max_y);
|
||||
pointer_anchored_ = true;
|
||||
} else {
|
||||
HoldPointerLocked(transform);
|
||||
}
|
||||
return NativeResultLocked(
|
||||
sink_.mouse_wheel(sink_.context, mouse_x_, mouse_y_, event.delta_y),
|
||||
RobloxInputEventKind::kMouseWheel);
|
||||
}
|
||||
|
||||
void RobloxInputRouter::HoldPointerLocked(
|
||||
const platform::SurfaceCoordinateTransform& transform) {
|
||||
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
|
||||
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
|
||||
if (!pointer_anchored_) {
|
||||
mouse_x_ = max_x / 2.0f;
|
||||
mouse_y_ = max_y / 2.0f;
|
||||
pointer_anchored_ = true;
|
||||
return;
|
||||
}
|
||||
mouse_x_ = std::clamp(mouse_x_, 0.0f, max_x);
|
||||
mouse_y_ = std::clamp(mouse_y_, 0.0f, max_y);
|
||||
}
|
||||
|
||||
RobloxInputDispatchResult RobloxInputRouter::HandleTouchLocked(
|
||||
const platform::TouchEvent& event) {
|
||||
if (sink_.touch == nullptr) {
|
||||
|
||||
@@ -195,6 +195,10 @@ Status RobloxWindowInputRuntime::UpdateTextFocusProperties(
|
||||
return runtime_.UpdateTextFocusProperties(generation, properties);
|
||||
}
|
||||
|
||||
void RobloxWindowInputRuntime::SetRawMouseEnabled(bool enabled) {
|
||||
runtime_.SetRawMouseEnabled(enabled);
|
||||
}
|
||||
|
||||
RobloxInputSnapshot RobloxWindowInputRuntime::Snapshot() const {
|
||||
return runtime_.Snapshot();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include "runtime/runtime_config.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -7,6 +8,7 @@
|
||||
#include <climits>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
|
||||
#ifndef MOCKTAIL_DISCORD_APPLICATION_ID
|
||||
#define MOCKTAIL_DISCORD_APPLICATION_ID ""
|
||||
@@ -39,13 +41,17 @@ bool IsDiscordApplicationId(std::string_view value) {
|
||||
}));
|
||||
}
|
||||
|
||||
bool IsDiscordText(std::string_view value, std::size_t maximum) {
|
||||
return !value.empty() && value.size() <= maximum &&
|
||||
bool IsDiscordOptionalText(std::string_view value, std::size_t maximum) {
|
||||
return value.size() <= maximum &&
|
||||
std::none_of(value.begin(), value.end(), [](unsigned char byte) {
|
||||
return byte < 0x20 || byte == 0x7f;
|
||||
});
|
||||
}
|
||||
|
||||
bool IsDiscordText(std::string_view value, std::size_t maximum) {
|
||||
return !value.empty() && IsDiscordOptionalText(value, maximum);
|
||||
}
|
||||
|
||||
bool ReadBoolean(const Environment& environment, std::string_view name,
|
||||
bool default_value, bool* valid) {
|
||||
const std::optional<std::string> value = environment.Get(name);
|
||||
@@ -229,6 +235,11 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
keyboard.has_value()) {
|
||||
config.input_capabilities_.keyboard_enabled = *keyboard;
|
||||
}
|
||||
if (const std::optional<bool> raw_mouse =
|
||||
InputEnabled(environment, "MOCKTAIL_RAW_MOUSE");
|
||||
raw_mouse.has_value()) {
|
||||
config.input_capabilities_.raw_mouse = *raw_mouse;
|
||||
}
|
||||
bool customized_device = false;
|
||||
config.device_profile_valid_ =
|
||||
config.device_profile_valid_ &&
|
||||
@@ -325,7 +336,7 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
}
|
||||
bool discord_booleans_valid = true;
|
||||
config.discord_rpc_.enabled = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_ENABLED", false,
|
||||
environment, "MOCKTAIL_DISCORD_RPC_ENABLED", true,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.show_place_name = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME", true,
|
||||
@@ -351,18 +362,40 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_JOINING", config.discord_rpc_.text.joining);
|
||||
config.discord_rpc_.text.playing = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_PLAYING", config.discord_rpc_.text.playing);
|
||||
config.discord_rpc_.text.state = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_STATE", config.discord_rpc_.text.state);
|
||||
config.discord_rpc_.text.unknown_place = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE",
|
||||
config.discord_rpc_.text.unknown_place);
|
||||
config.discord_rpc_valid_ = discord_booleans_valid &&
|
||||
// An empty presence field hides it, so only an unset variable keeps the
|
||||
// default.
|
||||
bool discord_presence_valid = true;
|
||||
for (const auto& [variable, field, maximum] : {
|
||||
std::tuple<std::string_view, std::string*, std::size_t>(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_TITLE",
|
||||
&config.discord_rpc_.text.title, 128),
|
||||
{"MOCKTAIL_DISCORD_RPC_TEXT_STATE",
|
||||
&config.discord_rpc_.text.state, 128},
|
||||
{"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE",
|
||||
&config.discord_rpc_.images.large, 512},
|
||||
{"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE_TEXT",
|
||||
&config.discord_rpc_.images.large_text, 128},
|
||||
{"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL",
|
||||
&config.discord_rpc_.images.small, 512},
|
||||
{"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL_TEXT",
|
||||
&config.discord_rpc_.images.small_text, 128},
|
||||
}) {
|
||||
if (std::optional<std::string> value = environment.Get(variable)) {
|
||||
*field = std::move(*value);
|
||||
}
|
||||
discord_presence_valid =
|
||||
discord_presence_valid && IsDiscordOptionalText(*field, maximum);
|
||||
}
|
||||
config.discord_rpc_valid_ =
|
||||
discord_booleans_valid && discord_presence_valid &&
|
||||
IsDiscordApplicationId(config.discord_rpc_.application_id) &&
|
||||
IsDiscordText(config.discord_rpc_.join_button_label, 32) &&
|
||||
IsDiscordText(config.discord_rpc_.text.browsing, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.joining, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.playing, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.state, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.unknown_place, 128);
|
||||
for (const std::string_view name : kUnsafeDetachedThreadOverrides) {
|
||||
if (LegacyEnabled(environment, name)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#include "runtime/runtime_config_bootstrap.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
@@ -16,7 +17,8 @@ namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kDefaultRuntimeConfig =
|
||||
R"yaml(# Mocktail configuration.
|
||||
R"yaml(# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Mocktail configuration.
|
||||
# The file is created once with mode 0600. Mocktail never replaces your edits.
|
||||
|
||||
# Integer: configuration schema version. Only version 1 is supported.
|
||||
@@ -64,7 +66,9 @@ graphics:
|
||||
# display, unlimited, or any positive integer; fixed values are forwarded to
|
||||
# DFIntTaskSchedulerTargetFps without a whitelist.
|
||||
# frame_rate_limit: -1
|
||||
# Optional presentation synchronization override: auto, on, or off.
|
||||
# Optional presentation synchronization override: auto, on, or off. auto
|
||||
# presents through the lowest-latency synchronized mode the driver offers
|
||||
# unless frame_rate_limit is unlimited.
|
||||
# vsync: off
|
||||
|
||||
performance:
|
||||
@@ -92,6 +96,14 @@ audio:
|
||||
# between boots; prefer the exact device name when it is unique.
|
||||
input_device: default
|
||||
|
||||
input:
|
||||
# Boolean (default: false): send mouse look deltas in the host's own units.
|
||||
# They are otherwise scaled by the window's pixel density divided by the
|
||||
# desktop display scale, which cancels to 1.0 unless fractional scaling makes
|
||||
# the two disagree. Turn this on when first-person aiming feels too slow or
|
||||
# too fast under fractional scaling. The pointer position is unaffected.
|
||||
raw_mouse: false
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
@@ -104,9 +116,9 @@ integrations:
|
||||
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
|
||||
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# Boolean (default: true): publish Nightcap activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
enabled: false
|
||||
enabled: true
|
||||
# Boolean (default: true): show the current Roblox experience name.
|
||||
show_place_name: true
|
||||
# Boolean (default: true): show how long the current session has run.
|
||||
@@ -119,12 +131,24 @@ integrations:
|
||||
public_servers_only: true
|
||||
# button_label: Join Server
|
||||
# English defaults. Uncomment only the lines you want to customize.
|
||||
# Text and image fields accept {place_name}, {place_icon}, and
|
||||
# {creator_name}. A field that expands an empty placeholder is left out,
|
||||
# and a "" title shows the app name.
|
||||
# title, state, and the images fields may be "" to hide them.
|
||||
# text:
|
||||
# browsing: Browsing experiences
|
||||
# joining: Joining an experience
|
||||
# playing: "{place_name}"
|
||||
# state: Playing Roblox
|
||||
# playing: "Playing {place_name}"
|
||||
# state: "by {creator_name}"
|
||||
# unknown_place: Unknown experience
|
||||
# title: Roblox
|
||||
# Images take an https:// URL, or an asset key uploaded to application_id.
|
||||
# large: "" hides both images; the small image needs a large one.
|
||||
# images:
|
||||
# large: "{place_icon}"
|
||||
# large_text: "{place_name}"
|
||||
# small: roblox_small
|
||||
# small_text: Roblox
|
||||
# Advanced: overrides the application bundled by the package maintainer.
|
||||
# application_id: 123456789012345678
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"window.title",
|
||||
"window.high_dpi",
|
||||
"input.touch_enabled",
|
||||
"input.raw_mouse",
|
||||
"compatibility.desktop_playability",
|
||||
"network.use_system_proxy",
|
||||
"network.proxy_host",
|
||||
@@ -204,6 +205,11 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"integrations.discord_rpc.text.playing",
|
||||
"integrations.discord_rpc.text.state",
|
||||
"integrations.discord_rpc.text.unknown_place",
|
||||
"integrations.discord_rpc.text.title",
|
||||
"integrations.discord_rpc.images.large",
|
||||
"integrations.discord_rpc.images.large_text",
|
||||
"integrations.discord_rpc.images.small",
|
||||
"integrations.discord_rpc.images.small_text",
|
||||
};
|
||||
for (const auto& [key, ignored] : yaml) {
|
||||
if (supported.find(key) == supported.end()) {
|
||||
@@ -433,6 +439,14 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
}
|
||||
(*environment)["MOCKTAIL_TOUCH_MODE"] = parsed ? "on" : "off";
|
||||
}
|
||||
if (const auto raw_mouse = value("input.raw_mouse"); raw_mouse.has_value()) {
|
||||
bool parsed = false;
|
||||
if (!ParseBoolean(*raw_mouse, &parsed)) {
|
||||
*error = "input.raw_mouse must be true or false";
|
||||
return false;
|
||||
}
|
||||
(*environment)["MOCKTAIL_RAW_MOUSE"] = parsed ? "on" : "off";
|
||||
}
|
||||
if (const auto desktop_playability =
|
||||
value("compatibility.desktop_playability");
|
||||
desktop_playability.has_value()) {
|
||||
@@ -545,8 +559,6 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_JOINING", 128},
|
||||
{"integrations.discord_rpc.text.playing",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_PLAYING", 128},
|
||||
{"integrations.discord_rpc.text.state",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_STATE", 128},
|
||||
{"integrations.discord_rpc.text.unknown_place",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE", 128},
|
||||
}) {
|
||||
@@ -565,6 +577,36 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
}
|
||||
(*environment)[std::string(field.variable)] = *configured;
|
||||
}
|
||||
// Presence fields may be empty: an empty value hides that field.
|
||||
for (const DiscordStringField& field : {
|
||||
DiscordStringField{"integrations.discord_rpc.text.title",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_TITLE", 128},
|
||||
{"integrations.discord_rpc.text.state",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_STATE", 128},
|
||||
{"integrations.discord_rpc.images.large",
|
||||
"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE", 512},
|
||||
{"integrations.discord_rpc.images.large_text",
|
||||
"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE_TEXT", 128},
|
||||
{"integrations.discord_rpc.images.small",
|
||||
"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL", 512},
|
||||
{"integrations.discord_rpc.images.small_text",
|
||||
"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL_TEXT", 128},
|
||||
}) {
|
||||
const std::optional<std::string> configured = value(field.yaml);
|
||||
if (!configured.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (configured->size() > field.maximum ||
|
||||
std::any_of(configured->begin(), configured->end(),
|
||||
[](unsigned char byte) {
|
||||
return byte < 0x20 || byte == 0x7f;
|
||||
})) {
|
||||
*error = std::string(field.yaml) +
|
||||
" must be bounded and contain no control bytes";
|
||||
return false;
|
||||
}
|
||||
(*environment)[std::string(field.variable)] = *configured;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -877,6 +919,9 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
SetEnvironmentValue(
|
||||
"MOCKTAIL_KEYBOARD_MODE",
|
||||
config.input_capabilities().keyboard_enabled ? "on" : "off", error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_RAW_MOUSE",
|
||||
config.input_capabilities().raw_mouse ? "on" : "off",
|
||||
error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DESKTOP_PLAYABILITY",
|
||||
config.desktop_playability() ? "1" : "0", error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_FRAME_RATE_LIMIT",
|
||||
@@ -928,7 +973,17 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_STATE",
|
||||
config.discord_rpc().text.state, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE",
|
||||
config.discord_rpc().text.unknown_place, error);
|
||||
config.discord_rpc().text.unknown_place, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_TITLE",
|
||||
config.discord_rpc().text.title, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_IMAGE_LARGE",
|
||||
config.discord_rpc().images.large, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_IMAGE_LARGE_TEXT",
|
||||
config.discord_rpc().images.large_text, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_IMAGE_SMALL",
|
||||
config.discord_rpc().images.small, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_IMAGE_SMALL_TEXT",
|
||||
config.discord_rpc().images.small_text, error);
|
||||
if (!base_exported) {
|
||||
return false;
|
||||
}
|
||||
|
||||